← All posts

Choose HTTP Error Statuses Without Hiding the Real Failure

A guide to distinguishing between 4xx and 5xx errors using a private photo album scenario to drive client-side recovery.

  • rest-api
  • learning-series
On this page

The Philosophy of Error Signaling

HTTP status codes are not just labels; they are instructions for the client. A 500 Internal Server Error is a failure of the server to handle a situation, whereas a 4xx code is a signal that the client must change its request to succeed. When an API collapses every domain error into a 500 or a generic 400, it strips the client of the ability to recover automatically.

Mapping Failures to Status Codes

400 covers malformed requests and can also cover input validation under a documented contract; 422 is a useful choice when well-formed content cannot be processed. 401 means valid authentication credentials are absent and requires WWW-Authenticate. 403 means the server refuses the request; it does not universally imply the caller authenticated successfully. A privacy policy may return 404 for inaccessible resources.

409 describes a conflict with current resource state. If a supplied If-Match precondition fails, use 412 Precondition Failed instead of calling every ETag mismatch 409. A version field checked by application logic may produce 409 under that API contract.

Worked Example: The Photo Album Trace

Scenario: A client attempts to update a photo in a private album. Below is the trace of different failure modes and the resulting client action.

RequestServer State/ConditionStatus CodeHeader/Body DetailClient Action
PATCH /photos/abc-123ID abc-123 is not a valid UUID400{"error": "Invalid ID format"}Fix request syntax
PATCH /photos/uuid-1Token expired401WWW-Authenticate: BearerRefresh token/Re-login
PATCH /photos/uuid-2User A tries to edit User B’s photo403 (or 404){"error": "Access denied"}Stop attempting access
PATCH /photos/uuid-3Resource does not exist404{"error": "Photo not found"}Remove from local cache
PATCH /photos/uuid-4 + If-MatchETag mismatch412{"error":"Precondition failed"}GET → reconcile → retry
PATCH /photos/uuid-5Caption is 10,000 chars422{"field": "caption", "msg": "Too long"}Prompt user to shorten
PATCH /photos/uuid-6Database connection timed out500{"error": "Internal error"}Retry with exponential backoff

Implementation Nuances

When implementing these, avoid the temptation to use a single catch (Exception e) block that returns 500. Instead, map specific domain exceptions to these codes.

Failure Case: The 401/403 Loop If a client receives a 401, it should attempt to refresh the token. If it receives a 403, it must not refresh the token, as the identity is already verified; the permission is simply missing. Refreshing on a 403 creates an infinite loop of useless requests.

Exercise

Scenario: You are building a file-sharing API. A user tries to upload a file to a folder. The following happens:

  1. The folder ID is provided as a string, but the system expects an integer.
  2. The user is logged in, but the folder is marked as “Read Only” for their group.
  3. The user tries to upload a file with the exact same name as one that already exists in that folder, and the business rule forbids duplicates.

Question: Which HTTP status codes should be returned for these three cases, and why?

Answer:

  1. 400 Bad Request: The input type is syntactically incorrect for the expected identifier.
  2. 403 Forbidden: The user is authenticated, but the specific action (write) is forbidden by the resource’s permissions.
  3. 409 Conflict: The request cannot be completed because of the current state of the resource (the existing filename conflict).

Further reading