Skip to main content
Back to module
Back-End Web APIs · HTTP and REST

Status codes and errors

Reading20mLesson 2 of 3 in this module

Pick the right HTTP status code for every outcome and return one consistent, safe error body.

Why it matters

Every HTTP response starts with a three-digit status code. Client code branches on it long before it reads the body: a mobile app shows "please log in" on a 401, retries on a 503, and highlights form fields on a 400. If your API answers 200 OK with { "error": "not found" } inside, every client has to parse the body to learn that something went wrong, and tools such as monitoring dashboards, caches and retry logic will treat the failure as a success.

The core idea: five families

The first digit tells you the family:

RangeFamilyWho should act
1xxInformationalRare in APIs; you can ignore them
2xxSuccessNobody: it worked
3xxRedirectionThe client should look somewhere else
4xxClient errorThe caller must change the request
5xxServer errorThe server failed; the request may be fine

The 4xx/5xx split is the most useful thing on this page. A 4xx says "do not retry the same request, it will fail again". A 5xx says "this is our fault; retrying later might work".

The codes you will use every week

Success

  • 200 OK: a normal successful response with a body (GET, or an update that returns the new item).
  • 201 Created: a POST created something. Return the new item, and optionally a Location: /courses/43 header.
  • 204 No Content: success with no body, common for DELETE.

Client errors

  • 400 Bad Request: the request is malformed or fails validation (missing field, wrong type, invalid JSON).
  • 401 Unauthorized: the caller is not authenticated. No session, expired token, bad credentials. The name is historical; read it as "unauthenticated".
  • 403 Forbidden: the caller is known but is not allowed to do this. A student trying to delete a course.
  • 404 Not Found: there is no such resource at this path.
  • 409 Conflict: the request clashes with the current state. Signing up with an email that is already taken, or approving a report that was already resolved.
  • 413 Payload Too Large: the body is bigger than the server accepts.
  • 422 Unprocessable Content: some APIs use this instead of 400 for "valid JSON, but the values break the rules". Pick one convention and stick to it.
  • 429 Too Many Requests: rate limited. Often sent with a Retry-After header.

Server errors

  • 500 Internal Server Error: an unexpected bug or crash.
  • 502 Bad Gateway / 503 Service Unavailable / 504 Gateway Timeout: something between the client and your code, or a service your code depends on, is down or slow.

Worked example: choosing the code

Walk through a PATCH /courses/42 request that renames a course:

No session cookie or token           -> 401
Logged in, but role is "student"     -> 403
Course 42 does not exist             -> 404
Body is {"title": ""}                -> 400 (title must not be empty)
Title already used by another course -> 409
Database is down                     -> 503 (or 500)
Everything worked                    -> 200 with the updated course

Notice the order of the checks: authenticate first, then authorize, then look up the resource, then validate. If you validate before you authenticate, an anonymous caller can probe your rules. Some APIs return 404 instead of 403 for private items so that outsiders cannot even learn that the item exists; that is a deliberate privacy choice.

A consistent error body

Status codes say what kind of failure happened; the body says what to fix. Choose one error shape and use it everywhere:

{
  "error": "Validation failed.",
  "details": [
    { "field": "title", "message": "Title is required." }
  ]
}

In Express, a handler sends it like this:

if (!title) {
  return res.status(400).json({
    error: "Validation failed.",
    details: [{ field: "title", message: "Title is required." }],
  });
}

Some teams follow RFC 9457 ("Problem Details for HTTP APIs"), which defines standard fields such as type, title, status and detail with the content type application/problem+json. That is a good default if you have no house style.

What not to put in an error

Error messages are read by whoever calls your API, including attackers. Keep them helpful but boring:

  • No stack traces or SQL in responses. Log them on the server instead.
  • No account enumeration. "No user with that email" versus "Wrong password" tells an attacker which emails are registered. A login endpoint should give one message, such as "Invalid email or password.", for both.
  • No secrets in any message, header or log line.

Common mistakes

  • 200 for everything, with an error field in the body.
  • 500 for bad input. A missing field is the caller's mistake (400), not a server crash. A route that throws on bad input usually means validation is missing.
  • Mixing up 401 and 403. Ask: "Do we know who this is?" No means 401; yes but not allowed means 403.
  • Different error shapes per route, so clients need a parser per endpoint.
  • Swallowing errors. Catching an exception and returning 200 hides real bugs from you and your monitoring.

Quick recap

  • 2xx worked, 4xx the caller must change something, 5xx the server failed.
  • Learn 200, 201, 204, 400, 401, 403, 404, 409, 429 and 500 by heart.
  • Check in order: authenticate, authorize, find the resource, validate, then act.
  • Use one error body shape, keep messages useful, and never leak internals.

The Do tab has a quiz of real scenarios: pick the right code for each.

Also in this lesson: