Unit 06.01: Errors clients can handle
An error body needs a stable code a client can branch on.
Machine-readable alongside human-readable
A lookup returning a structured error with a code and the offending identifier.
The code shows both the success and the failure.
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
app = FastAPI()
INVOICES = {"INV-1": {"id": "INV-1", "amount": 240.0}}
@app.get("/invoices/{invoice_id}")
def get_invoice(invoice_id: str) -> dict:
invoice = INVOICES.get(invoice_id)
if invoice is None:
raise HTTPException(status_code=404,
detail={"error": "invoice_not_found",
"invoice_id": invoice_id})
return invoice
client = TestClient(app)
for invoice_id in ("INV-1", "INV-9"):
r = client.get(f"/invoices/{invoice_id}")
print(f"{invoice_id}: {r.status_code} {r.json()}")
print("""
The error body carries a stable machine-readable code alongside the human
text. A client can branch on `invoice_not_found`; it cannot branch on a
sentence that may be reworded next release.
""")
invoice_not_found is stable; the sentence beside it can be reworded, translated or improved without breaking anyone. A client branching on prose breaks the first time someone fixes a typo.
Including the identifier is what makes the error actionable - the caller knows which of the twenty invoices they requested was missing.
The mistake this prevents
The mistake is returning only a human sentence. It reads well in a browser and gives an automated caller nothing to switch on, so they end up matching on substrings - and then your wording is part of the contract whether you meant it or not.
Takeaway
Every error body needs a stable machine-readable code and enough context to act on. Prose is for humans and must not be what clients branch on.
