Skip to content
3 min read · 653 words

Error Handling

Three failure philosophies coexist deliberately: chat degrades gracefully, tools fail loudly, and workflows fail loudly with a full trace. All three leave evidence.

Status-code map

CodeMeaning hereProducers
200Success — including chats whose LLM call failed (see below)everywhere
400Client input invalid at the domain levelbad upload (type/encoding/empty), invalid task status, delete-without-force
403Agent lacks the required toolemail/workflow/search/ask endpoints via agent_has_tool
404Agent / task / workflow-run not foundnearly every endpoint's first guard
422Body failed Pydantic validationFastAPI automatic
500LLM/embedding/vector failure, JSON-parse failure, or unexpected exceptiontool & workflow endpoints

The three philosophies

1 · Chat: degrade, don't fail

python
try:
    assistant_text = generate_agent_response(...)
    status, error_message = "success", None
except Exception as e:
    assistant_text = "Sorry, I could not generate a response right now."
    status, error_message = "failed", str(e)

The user gets a polite fallback and HTTP 200; the failure is recorded on the AgentRun (status="failed", error_message). Rationale: a chat UI should never show a stack trace, and the conversation history stays coherent. Where to look when it happens: GET /agents/{id}/runs.

2 · Tools: fail loudly, log first

Email analyze, document search, and RAG ask wrap their work in try/except; on failure they log a failed ToolCall (with error_message), commit it, then raise HTTPException(500, detail=str(e)):

python
except Exception as e:
    log_tool_call(db, agent.id, tool_name=..., success=False, error_message=str(e))
    db.commit()
    raise HTTPException(status_code=500, detail=str(e))

The commit-before-raise matters: the evidence survives the rollback semantics of the failed request.

3 · Workflows: fail loudly with a trace

The multi-agent workflow adds two more layers on top of the tool pattern — the WorkflowRun is flipped to failed with the error message, and a workflow_error step is appended so the trace shows exactly where the pipeline died:

python
except Exception as e:
    log_tool_call(..., tool_name="multi_agent_email_workflow", success=False, ...)
    workflow_run.status = "failed"
    workflow_run.error_message = str(e)
    log_workflow_step(..., step_name="workflow_error", status="failed", error_message=str(e))
    db.commit()
    raise HTTPException(status_code=500, detail=str(e))

Because the run was created (and committed) with status="running" before the pipeline started, even a crash that somehow skipped the handler would leave a discoverable stuck run.

Common failure sources

SourceTypical exceptionSurfaces as
Missing/invalid OPENAI_API_KEYValueError("OPENAI_API_KEY is missing...")chat fallback, or 500 on tools
Model returns non-JSON where JSON demandedValueError("... did not return valid JSON: ...")500; raw model output included in detail
OpenAI network/rate-limit errorsSDK exceptionschat fallback or 500
Empty-context RAG answer call (unreachable via API today)NameError in generate_rag_answerwould be 500 — see Known Issues
Wrong working directoryfresh empty DB / vector store, not an exception"missing data" — see Troubleshooting

Client-side handling

The frontend has its own two layers (Frontend):

  • Server components catch fetch failures and render inline error panels with a retry link (dashboard, agents list) — plus Next.js route-level error.tsx as the last resort.
  • The API proxy converts unreachable-backend errors into 502 {"detail": "Unable to reach ..."} and normalizes FastAPI's detail (string or validation array) into one message for the form UI (API Proxy).

Gaps and sharp edges

  • detail: str(e) leaks internals — exception text (paths, model output) goes straight to the client. Fine locally; sanitize before any public deployment (Security).
  • No retries anywhere — a transient OpenAI hiccup fails the whole request. Bounded retries on JSON-parse and rate-limit errors are the cheapest win available.
  • Upload has no ToolCall logging and a partial-failure window (documents router).
  • No global exception handler — anything not caught locally becomes FastAPI's default 500.