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
| Range | Meaning | Whose problem? |
|---|---|---|
| 2xx | Success | - |
| 3xx | Go somewhere else (redirect, use the cache) | - |
| 4xx | The client sent a bad request | Fix the request |
| 5xx | The server failed while handling it | Fix 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
| Code | When | Example |
|---|---|---|
| 200 OK | Regular success with a body | Listing items, returning the result of an update |
| 201 Created | You created a new resource | Sign-up, creating a post |
| 204 No Content | Success with no body to return | Deleting, 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?
| Code | Meaning | Example |
|---|---|---|
| 400 Bad Request | The request can't be parsed | Broken JSON syntax, a required parameter of the wrong type entirely |
| 422 Unprocessable Content | The format is fine, but the content breaks the rules | Invalid 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
| Code | Meaning | What the client should do |
|---|---|---|
| 401 Unauthorized | We don't know who you are (not logged in, expired token) | Send to login or refresh the token |
| 403 Forbidden | We know who you are, but you lack permission | Show 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
| Code | When | Example |
|---|---|---|
| 404 Not Found | The resource doesn't exist | A deleted post |
| 405 Method Not Allowed | The method isn't supported at that URL | DELETE on a read-only endpoint |
| 409 Conflict | Conflicts with the current state | Signing up with an email already in use, concurrent edit conflict |
| 429 Too Many Requests | Rate limit exceeded | Too 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
| Code | Meaning | Example |
|---|---|---|
| 500 Internal Server Error | Unexpected error in server code | An unhandled exception |
| 502 Bad Gateway | A middle server (proxy, gateway) got an invalid response from the upstream server | The upstream app server crashed and the connection dropped |
| 503 Service Unavailable | The server temporarily can't handle requests | Maintenance, overload |
| 504 Gateway Timeout | A middle server timed out waiting for the upstream server | A 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:
| Code | Meaning | Request after the redirect |
|---|---|---|
| 301 Moved Permanently | Moved permanently | GET, body dropped |
| 302 Found | Moved temporarily | GET, body dropped |
| 303 See Other | Go GET something else | GET (intended) |
| 307 Temporary Redirect | Moved temporarily, method kept | POST, body kept |
| 308 Permanent Redirect | Moved permanently, method kept | POST, 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
| Situation | Status code |
|---|---|
| Successful read | 200 |
| Successful create | 201 + Location |
| Success, no body | 204 |
| Malformed request | 400 |
| Validation failure | 422 (or consistently 400) |
| Login required / token expired | 401 |
| No permission | 403 (404 to hide existence) |
| Duplicate, state conflict | 409 |
| Rate limit exceeded | 429 + Retry-After |
| Server exception | 500 |
| Temporary maintenance, overload | 503 |
| Moving an API endpoint | 308 (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.