Advertisement

Most developers know 404 means "not found" and 500 means "something broke", but HTTP status codes are a rich, precise language for describing what happened to a request. Using them accurately — as a server, and reading them correctly as a client — makes web systems far easier to build against and to debug.

The codes are organised into families, and knowing the families is most of the battle.

The families

Status codes group into ranges by meaning. The 200s signal success — the request worked. The 300s indicate redirection — the resource is elsewhere. The 400s mean the client made an error, such as a bad request or a missing resource. The 500s mean the server failed while handling an otherwise valid request. Just knowing which family a code belongs to tells you whose problem it is.

That last distinction is especially useful: a 400-range code points at the request, a 500-range code points at the server.

Advertisement

Why precision matters

Returning the right code lets clients respond intelligently without parsing your prose. A "not found" is handled differently from a "you are not allowed", which differs again from "you sent something invalid" or "try again later". Collapsing all failures into a generic error throws away information that a well-chosen code would convey for free.

Good status codes also make debugging faster, because the code alone often localises the problem before you read a single log line.

Using them well

As a server, choose the most specific accurate code for each outcome, distinguishing client mistakes from server failures and success from redirection. As a client, branch on the code family first and handle the meaningful specifics, rather than treating every non-success identically.

Status codes are a shared vocabulary the whole web already speaks. Using them precisely is a small discipline that pays off every time someone — including future you — has to figure out what a response actually meant.

Advertisement

401 versus 403: a distinction worth making correctly

401 Unauthorized specifically means the request lacks valid authentication credentials at all — the server does not know who is asking — while 403 Forbidden means the server knows exactly who is asking and has decided that identity is not permitted to access this specific resource; conflating the two, returning 403 for a request with no credentials or 401 for a request whose valid credentials simply lack sufficient permission, gives client code the wrong signal about what actually needs fixing: obtaining credentials at all, versus obtaining different, more privileged ones.

Why 404 is sometimes the deliberately chosen wrong answer, for good reason

Some APIs deliberately return 404 rather than 403 for a resource that exists but the requester is not authorized to know exists at all — returning 403 would confirm the resource's existence to an unauthorized party, leaking information the API is specifically trying to protect, while 404 gives no such confirmation either way. This is a legitimate, deliberate security trade-off rather than a mistake, and recognizing it as intentional explains why some APIs' 403-versus-404 behavior does not follow the textbook distinction described above quite as cleanly as expected.

Advertisement

The 3xx family: why a redirect code is not just about the destination URL

A 301 Moved Permanently tells clients and search engines to update their own records to point at the new location going forward, while a 302 Found signals a temporary redirect that should not be cached or treated as the resource's permanent new home — using 301 for a genuinely temporary redirect can cause search engines and caching clients to prematurely and permanently forget the original URL, while using 302 for a permanent move misses out on the SEO benefit of consolidating link authority at the new, correct location, which is exactly why choosing the wrong one of the two has consequences that outlast the redirect itself.

5xx codes as a signal about where the fault actually lies

The 4xx and 5xx boundary is not arbitrary: a 4xx code asserts the client's request was itself the problem — malformed, unauthorized, referencing something that does not exist — while a 5xx code asserts the server failed to properly handle an otherwise valid request, which is why returning a 500 for what is actually a client validation failure, or a 400 for what is actually an internal server crash, misdirects whoever is debugging the failure toward fixing the wrong side of the interaction entirely.

Why 422 exists as a more specific alternative to a bare 400

422 Unprocessable Entity signals that a request was syntactically well-formed and understood by the server, but semantically invalid — a JSON body that parses correctly but fails a business validation rule, like an email field containing a syntactically valid but already-registered address — which is a more specific, more useful signal than the generic 400 Bad Request, which technically covers both a malformed request the server could not even parse and a well-formed one that simply failed validation; using 422 specifically for the second case lets client code distinguish a parsing problem from a validation problem without needing to inspect response body text to tell the two apart.

Why 429 needed its own dedicated code rather than reusing an existing one

429 Too Many Requests was added specifically because rate limiting became common enough that overloading an existing code like 403 Forbidden for this purpose caused real confusion — a client rejected for exceeding a rate limit needs fundamentally different handling than one rejected for lacking permission entirely, namely waiting and retrying rather than assuming the request will never succeed, and having a dedicated code lets client libraries implement that specific, correct retry behavior automatically rather than needing to guess at the reason behind a more generic rejection code.

Why 204 exists for a success that deliberately has nothing to say

204 No Content signals a fully successful request that intentionally returns no response body at all — a DELETE that succeeded, with nothing further to report — which is a meaningfully different signal than 200 OK with an empty body, since 204 explicitly tells a client not to expect or attempt to parse any body content, while a 200 with an accidentally empty body could equally represent a bug in the response-generation code, leaving the client uncertain whether the absence of content is intentional or itself an error.

Why 418 became a genuinely famous joke status code that still ships in some libraries

418 I'm a Teapot originated as an April Fools' joke in an early HTTP specification draft and was never meant to see real production use, yet it persisted long enough to become a widely recognized reference, with some HTTP client libraries and testing frameworks still implementing support for it specifically as an easter egg — its survival says less about HTTP itself and more about how thoroughly a good technical joke, once documented, tends to outlive the context that originally made it funny.

Why a status code alone is never sufficient documentation for an API's error behavior

Knowing that an endpoint might return 404 does not tell a consumer which of several possible reasons caused it in a given case, and relying purely on the numeric status code as documentation leaves genuine ambiguity a well-documented, structured error body is meant to resolve — the status code and the error response body work together, one classifying the broad category of failure and the other providing the specific detail, and documentation describing only the former leaves an API meaningfully harder to integrate against correctly.

Why some APIs return 200 with an error embedded in the body, and why that is usually a mistake

An API that always returns HTTP 200 regardless of whether the operation actually succeeded, embedding a separate success or failure flag inside the JSON response body instead, defeats the purpose of having a standardized status code layer at all — generic HTTP tooling like caching proxies, monitoring systems, and API gateways all rely on the status code to understand whether a request succeeded, and hiding that information inside a body only the application layer parses makes an entire category of otherwise-reusable infrastructure blind to the actual outcome of every request.

Why 304 Not Modified is the status code that makes conditional caching actually work

A client that already has a cached copy of a resource can send a conditional request including that copy's ETag, and if the resource has not changed, the server responds with 304 Not Modified and an empty body rather than resending the full content again — this is the concrete mechanism underneath the ETag-based revalidation discussed in this library's HTTP caching article, and it is precisely the status code that lets a cache confirm freshness without paying the bandwidth cost of redownloading content that has not actually changed at all.

Why the specific 1xx and lesser-known status codes rarely come up but exist for real reasons

1xx informational codes, like 100 Continue, let a client check with the server before sending a large request body, avoiding wasted upload bandwidth if the server was always going to reject it outright — these codes see far less everyday use than the familiar 2xx, 4xx, and 5xx families, but they exist to solve a genuinely specific problem, and their rarity in day-to-day API work is a reflection of how narrow that problem is, not evidence that the broader status-code specification has any unnecessary or purely decorative parts.

Why monitoring status code distribution over time reveals problems a single request never would

Watching the aggregate proportion of 4xx versus 5xx responses across all traffic, tracked over time, reveals patterns invisible from inspecting any single request in isolation — a sudden rise in 400s might indicate a client library bug sending malformed requests after a recent release, while a rise in 500s more likely points at a server-side regression, and this distinction, visible only in aggregate, is often the very first signal an on-call engineer sees before any more detailed investigation begins.

Why status codes are a shared vocabulary across an entire industry, not just one API's convention

Using status codes correctly and consistently is valuable specifically because it taps into a vocabulary every HTTP client, proxy, and monitoring tool already understands without needing any API-specific documentation at all — deviating from the standard meanings trades away that universal, free interoperability for whatever narrow convenience the deviation seemed to offer in the moment, which is rarely a good trade once the full cost of losing standard tooling compatibility is actually counted.

Why teaching status codes by their number ranges, not by memorizing each one individually, generalizes better

Rather than memorizing what every individual code means, learning the broad meaning of each hundred-range — 2xx success, 3xx redirection, 4xx client error, 5xx server error — lets a developer make a reasonable, correct guess about an unfamiliar code's general meaning on sight, which generalizes far better than rote memorization of a fixed list, especially once a lesser-used code is encountered for the first time in an unfamiliar API.