HTTP Status Codes
Every HTTP response carries a three-digit status code that says how the request went. Learn the five classes and the codes you will actually meet.
Prerequisites: HTTP Request and Response
Learning objectives
- Name the five status code classes (1xx–5xx) and what each one signals.
- Recognize the most common codes such as 200, 201, 204, 301, 302, 304, 400, 401, 403, 404, 409, 422, 429, 500, 502 and 503.
- Distinguish 401 (authentication) from 403 (authorization) and 301 (permanent) from 302 (temporary).
- Choose the right status code when designing an API response.
Simple explanation
When you ask a server for something, it does not only hand back the data — it also hands back a short verdict on how the request went. That verdict is a three-digit status code. Think of it like the light on a traffic signal: green means *"all good, here it is"*, yellow means *"go somewhere else for this"*, and red means *"something went wrong"* — and the red splits into *"your mistake"* and *"my mistake"*.
You have already seen 200 OK and the dreaded 404 Not Found. There are only a few dozen you meet in daily work, and they follow a very predictable pattern.
Technical explanation
Every HTTP response begins with a status line like HTTP/1.1 200 OK. The number is grouped into five classes by its first digit:
- 1xx Informational — the request was received and the process is continuing (e.g.
100 Continue,101 Switching Protocols). You rarely handle these by hand. - 2xx Success — the request was received, understood, and accepted.
- 3xx Redirection — further action (usually going to another URL) is needed to complete the request.
- 4xx Client Error — the request was faulty: bad syntax, missing auth, or a resource that isn't there. The client should change something and retry.
- 5xx Server Error — the request looked fine but the server failed to fulfill it. The client did nothing wrong.
That 4xx-vs-5xx split — *whose fault is it?* — is the single most useful thing to internalize.
The codes you actually meet
2xx Success
200 OK— the standard "it worked" forGET,PUT,PATCH, andDELETEthat return a body.201 Created— a new resource was created (typically afterPOST). ALocationheader points to the new resource.204 No Content— success, but there is deliberately no body to return (common forDELETEor aPUTthat returns nothing).
3xx Redirection
301 Moved Permanently— the resource now lives at a new URL forever. Browsers and search engines update their bookmarks/index. Use it for permanent domain or path changes.302 Found— a temporary redirect; the resource is elsewhere *for now* but the original URL still owns it. Don't let clients cache it as permanent.304 Not Modified— used with caching. The client sent a conditional request (If-None-Match/If-Modified-Since); the resource hasn't changed, so the server returns no body and the client reuses its cached copy.
4xx Client Error
400 Bad Request— the request is malformed and the server can't parse it (invalid JSON, missing required field).401 Unauthorized— you are not authenticated. Despite the name, this means "I don't know who you are — log in." It usually comes with aWWW-Authenticateheader.403 Forbidden— you are authenticated but not allowed. The server knows who you are and is refusing anyway. Re-logging-in will not help.404 Not Found— the resource does not exist (or the server won't admit it does).409 Conflict— the request conflicts with the current state, e.g. creating a user with an email that already exists, or a version/edit conflict.422 Unprocessable Entity— the syntax is fine but the *semantics* are invalid: the JSON parsed, but "age" was -5. Widely used by APIs for validation errors.429 Too Many Requests— rate limiting. You sent too many requests in a window; aRetry-Afterheader often tells you how long to wait.
5xx Server Error
500 Internal Server Error— a generic, unhandled failure on the server (an uncaught exception). It says nothing more specific.502 Bad Gateway— a server acting as a proxy/gateway got an invalid response from an upstream server.503 Service Unavailable— the server is temporarily overloaded or down for maintenance. Often paired withRetry-After.
Why it matters
Status codes are the contract between client and server. A frontend that treats every non-200 the same way cannot tell "please log in" (401) from "you can't touch this" (403) from "our server crashed" (500). Getting them right means correct retry logic, correct caching, correct error messages to users, and search engines that index your site properly.
How it works
The server picks a code as it builds the response and writes it into the status line. The client reads that number first and branches on it:
- Read the status code from the response (
res.statusin fetch,response.status_codein requests, etc.). - Branch by class: 2xx → use the body; 3xx → follow
Location(browsers do this automatically); 4xx → fix the request; 5xx → back off and maybe retry. - For 4xx/5xx, read the body for a machine-readable error detail if the API provides one.
Real-world analogy
Imagine mailing a package request to a warehouse.
- 200/201 — it arrives and they ship your item (created a new one for 201).
- 301/302 — "we moved; send future mail to this new address" (301 forever, 302 just for now).
- 304 — "nothing changed since last time, keep the copy you already have."
- 401 — "we can't process this — you didn't sign the form" (prove who you are).
- 403 — "we know it's you, but you're not on the approved list."
- 404 — "no such item in our catalog."
- 429 — "you're flooding us with requests; slow down."
- 500/503 — "our warehouse caught fire / is closed for maintenance" — nothing you did.
Important terminology
- Status class — the first digit of the code (1–5) that groups all codes by broad meaning.
- Reason phrase — the human-readable text after the number (
OK,Not Found). It is informational only; clients branch on the number, not the text. - Authentication vs authorization — 401 is about *who you are* (identity/login); 403 is about *what you're allowed to do* (permissions).
- Conditional request — a request with
If-None-Match/If-Modified-Sincethat lets the server answer304 Not Modifiedand save bandwidth.
Key definitions
- Status class (1xx–5xx)
- The first digit groups every code: 1xx info, 2xx success, 3xx redirect, 4xx client error, 5xx server error.
- 401 vs 403
- 401 means not authenticated (who are you?); 403 means authenticated but not permitted (you may not do this).
- 301 vs 302
- 301 is a permanent redirect (clients/search engines update the URL); 302 is a temporary one (the original URL still owns the resource).
- 304 Not Modified
- A cache response to a conditional request: the resource is unchanged, so no body is sent and the client reuses its cached copy.
Must memorize
Must memorize
- 1xx info, 2xx success, 3xx redirect, 4xx client error, 5xx server error.
- 4xx = you (the client) messed up; 5xx = the server messed up.
- 401 = not authenticated (log in); 403 = authenticated but forbidden (no permission).
- 301 = permanent move; 302 = temporary move; 304 = use your cache.
Real-world example
Creating a resource in a REST API
A POST /api/orders that succeeds returns 201 Created with a Location header pointing to /api/orders/1001. If the same order id already exists, the server returns 409 Conflict; if the payload parsed but "quantity" was 0, it returns 422 Unprocessable Entity with the field-level errors in the body.
A 429 during a traffic spike
A public API allows 100 requests per minute per key. A client that exceeds it gets 429 Too Many Requests with Retry-After: 30. A well-behaved client waits 30 seconds before retrying instead of hammering the endpoint.
Code example
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api", error="invalid_token"
Content-Type: application/json
Content-Length: 52
{
"error": "invalid_token",
"message": "Token expired"
}A raw 401 response asking the client to authenticate.
const res = await fetch('https://api.example.com/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + token,
},
body: JSON.stringify({ item: 'coffee', quantity: 2 }),
});
if (res.status === 201) {
const order = await res.json(); // created
console.log('Created at', res.headers.get('Location'));
} else if (res.status === 401) {
redirectToLogin(); // not authenticated
} else if (res.status === 403) {
showMessage('You do not have permission.'); // authenticated, no access
} else if (res.status === 422) {
const { errors } = await res.json(); // validation failed
showFieldErrors(errors);
} else if (res.status >= 500) {
showMessage('Server error, please try again later.'); // not your fault
}Branching on res.status to handle each class correctly.
Common mistakes
Mistake
Returning 200 OK for everything and putting an "error" flag in the JSON body.
Do this instead
Use the status code as the primary signal. Reserve 2xx for success and return 4xx/5xx for failures so clients, proxies, and monitoring behave correctly.
Mistake
Using 401 when the user is logged in but simply lacks permission.
Do this instead
That case is 403 Forbidden. Use 401 only when authentication is missing or invalid — i.e. the server does not yet know who the caller is.
Interview questions
Quiz
Flashcards
Show answer · Space