Using HTTP Status Codes Correctly: 400 vs 422, 401 vs 403, and 200 vs 201 vs 204

If your errors come back as 200 with success: false, clients and monitoring tools can't tell anything failed. Which status code fits which situation, the difference between easily confused codes like 401 and 403 or 400 and 422, and the redirect trap where 301 turns POST into GET.

It failed, but it's 200 OK?

You've probably seen an API response like this:

HTTP/1.1 200 OK

{ "success": false, "message": "User does not exist" }

The request failed, but the status code says "success." That causes a chain of problems:

  • On the frontend, fetch's res.ok is true, so every response body has to be opened and checked for success.
  • Error monitoring, load balancers, and CDNs can't recognize failures. The dashboard shows a 0% error rate while users keep failing.
  • A failure response might even get cached as a "success."

HTTP status codes are an agreement that tells you what kind of result you got without opening the body. Browsers, proxies, monitoring tools, and client libraries all decide what to do based on this number.


The first digit gets you halfway

RangeMeaningWhose problem?
2xxSuccess-
3xxGo somewhere else (redirect, use the cache)-
4xxThe client sent a bad requestFix the request
5xxThe server failed while handling itFix the server

The most important split is 4xx vs 5xx. Bad input is 4xx; an exception in server code is 5xx. Return 500 for a user's typo and your outage alert goes off, waking up the wrong person at 3 a.m.


Success: 200, 201, 204

CodeWhenExample
200 OKRegular success with a bodyListing items, returning the result of an update
201 CreatedYou created a new resourceSign-up, creating a post
204 No ContentSuccess with no body to returnDeleting, updates with no response body

With 201, it's customary to put the new resource's URL in the Location header.

HTTP/1.1 201 Created
Location: /users/42

{ "id": 42, "name": "kim" }

A 204 response has no body. If your frontend calls await res.json() out of habit, you'll get a SyntaxError. For requests that return 204, like delete endpoints, check res.status === 204 first.


The most confusing 4xx codes

400 vs 422: broken format, or invalid content?

CodeMeaningExample
400 Bad RequestThe request can't be parsedBroken JSON syntax, a required parameter of the wrong type entirely
422 Unprocessable ContentThe format is fine, but the content breaks the rulesInvalid email format, password shorter than 8 characters

Plenty of teams return 400 for every validation failure, and that's not wrong. What matters is being consistent within one API.

401 vs 403: we don't know you, or we know you but no

CodeMeaningWhat the client should do
401 UnauthorizedWe don't know who you are (not logged in, expired token)Send to login or refresh the token
403 ForbiddenWe know who you are, but you lack permissionShow a "no permission" message (logging in again won't help)

The name Unauthorized is confusing, but 401 actually means "not authenticated." Mix the two up and a user without permission gets bounced to the login screen in an endless loop.

Hiding existence with 404

When the fact that something exists is itself sensitive, like someone else's private post, you may return 404 instead of 403. A 403 effectively says "it's there, but you can't see it." GitHub returns 404 for private repositories you can't access.

Other common 4xx codes

CodeWhenExample
404 Not FoundThe resource doesn't existA deleted post
405 Method Not AllowedThe method isn't supported at that URLDELETE on a read-only endpoint
409 ConflictConflicts with the current stateSigning up with an email already in use, concurrent edit conflict
429 Too Many RequestsRate limit exceededToo many login attempts (say how long to wait with Retry-After)

Pick a situation in the preview below.


5xx: server failures come in kinds too

CodeMeaningExample
500 Internal Server ErrorUnexpected error in server codeAn unhandled exception
502 Bad GatewayA middle server (proxy, gateway) got an invalid response from the upstream serverThe upstream app server crashed and the connection dropped
503 Service UnavailableThe server temporarily can't handle requestsMaintenance, overload
504 Gateway TimeoutA middle server timed out waiting for the upstream serverA slow DB query kept the app server from responding

You usually don't send 502 or 504 yourself; middle servers like Nginx or a load balancer return them for you. When you see them, suspect the link "between the front proxy and the app server."

Don't put stack traces or DB queries in the body of a 500 response. That hands attackers a map of your internals. Log the details on the server and return something like a trace ID in the response.


The 3xx trap: redirects that turn POST into GET

Redirect codes don't just differ in "where to go"; they differ in whether the original method is kept. Redirecting a real POST request (body name=kim) with each code gives:

CodeMeaningRequest after the redirect
301 Moved PermanentlyMoved permanentlyGET, body dropped
302 FoundMoved temporarilyGET, body dropped
303 See OtherGo GET something elseGET (intended)
307 Temporary RedirectMoved temporarily, method keptPOST, body kept
308 Permanent RedirectMoved permanently, method keptPOST, body kept

For historical reasons, browsers turn POST into GET on 301 and 302. So if you move an API endpoint and put a 301 on the old URL, POST bodies silently disappear.

  • Moving page URLs (SEO): 301 or 308
  • Moving API endpoints: 308, which keeps the method and body (307 if temporary)
  • Sending the user to a result page after a form submit (to prevent resubmission on refresh): 303

Summary: why this is worth knowing

SituationStatus code
Successful read200
Successful create201 + Location
Success, no body204
Malformed request400
Validation failure422 (or consistently 400)
Login required / token expired401
No permission403 (404 to hide existence)
Duplicate, state conflict409
Rate limit exceeded429 + Retry-After
Server exception500
Temporary maintenance, overload503
Moving an API endpoint308 (301 turns POST into GET)

Use status codes properly and clients can decide what to do before opening the body (log in again, retry, fix the input), and monitoring tools count failures as failures. What the error body should look like is covered next, in designing API error responses.