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
| Code | Meaning here | Producers |
|---|---|---|
200 | Success — including chats whose LLM call failed (see below) | everywhere |
400 | Client input invalid at the domain level | bad upload (type/encoding/empty), invalid task status, delete-without-force |
403 | Agent lacks the required tool | email/workflow/search/ask endpoints via agent_has_tool |
404 | Agent / task / workflow-run not found | nearly every endpoint's first guard |
422 | Body failed Pydantic validation | FastAPI automatic |
500 | LLM/embedding/vector failure, JSON-parse failure, or unexpected exception | tool & workflow endpoints |
The three philosophies
1 · Chat: degrade, don't fail
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)):
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:
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
| Source | Typical exception | Surfaces as |
|---|---|---|
Missing/invalid OPENAI_API_KEY | ValueError("OPENAI_API_KEY is missing...") | chat fallback, or 500 on tools |
| Model returns non-JSON where JSON demanded | ValueError("... did not return valid JSON: ...") | 500; raw model output included in detail |
| OpenAI network/rate-limit errors | SDK exceptions | chat fallback or 500 |
| Empty-context RAG answer call (unreachable via API today) | NameError in generate_rag_answer | would be 500 — see Known Issues |
| Wrong working directory | fresh 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.tsxas the last resort. - The API proxy converts unreachable-backend errors into
502 {"detail": "Unable to reach ..."}and normalizes FastAPI'sdetail(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
ToolCalllogging and a partial-failure window (documents router). - No global exception handler — anything not caught locally becomes FastAPI's default 500.
Related pages
- Observability — where the evidence lands
- Troubleshooting — symptom-first diagnosis
- Known Issues — the bugs themselves