New to Rust? Grab our free Rust for Beginners eBook Get it free →
Handle HTTP Status Codes in Node.js

A route that creates a resource needs to tell its caller what happened, while a delete may have no content to return. HTTP status codes describe the request outcome and whether the response includes content.
This article covers common status meanings and shows how to set a status in Express with res.status() or shared error middleware.
TL;DR
Choose a status code that describes the request outcome, then return a body that gives the client a safe next step.
- Use res.status() for route-specific responses.
- Use an error object such as Boom when errors need to travel through shared middleware.
- Use http-status when named constants make numeric codes clearer.
What HTTP status codes tell an API client
An HTTP status code is a three-digit result attached to a response. The first digit groups the result: 2xx means success, 3xx concerns redirection, 4xx points to a request or client-side issue, and 5xx reports a server-side failure.
Use the code to describe the request outcome, not to encode every detail. A JSON body can carry a stable application error identifier and a message suitable for the caller, while logs retain diagnostic detail. That split matters because server exceptions can contain paths, query details or internal data that should not be sent to an API client.
| Code | Use it when | Response detail |
|---|---|---|
| 200 OK | A request succeeded and has a response representation | Return the resource or result |
| 201 Created | The request created a resource | Return the resource, often with its location |
| 204 No Content | The request succeeded and there is no response body | End the response without JSON |
| 400 Bad Request | Input is malformed or invalid for the request | Explain which input needs correction |
| 401 Unauthorized | The caller has not supplied valid authentication | Do not confuse it with a permissions denial |
| 403 Forbidden | The caller is identified but lacks permission | Avoid disclosing a protected resource |
| 404 Not Found | The route or requested resource cannot be found | Use a consistent not-found body |
| 409 Conflict | The request conflicts with current resource state | Describe the conflict the caller can resolve |
| 422 Unprocessable Content | The syntax is valid but the submitted data fails semantic validation | Return field-level validation details when safe |
| 429 Too Many Requests | A rate limit applies | Include retry guidance when known |
| 500 Internal Server Error | An unexpected server failure occurred | Return a generic message and log the cause privately |
| 503 Service Unavailable | The service cannot handle the request temporarily | Use Retry-After when a recovery estimate exists |
Available HTTP response formats
HTTP status codes are independent of the representation format. An API can send JSON, plain text, HTML, or no body, provided its headers and body describe the response accurately. JSON is common for API clients because fields can carry a stable error code and structured validation detail.
| Format | Useful when | Header to check |
|---|---|---|
| JSON | Clients need machine-readable fields | Content-Type: application/json |
| Plain text | A short human-readable response is enough | Content-Type: text/plain |
| HTML | The endpoint serves a browser page | Content-Type: text/html |
| No content | The result has no representation to return | No body for 204 |
Choose one format per endpoint unless content negotiation is an intentional part of the API. A client should be able to rely on the Content-Type header instead of guessing from the first character of the body. On an error path, keep the same format contract as successful responses so a caller can use one parser.
Registration procedures for consistent status handling
Register shared error middleware after route handlers and before any final not-found response that should become an error. Express identifies error middleware by its four-argument signature. A handler mounted too early cannot catch errors from routes registered after it.
app.use('/api', apiRoutes);
app.use((req, res, next) => {
next(Boom.notFound('Route not found'));
});
app.use(errorHandler);This order gives API routes the first chance to respond, turns an unmatched API path into a known not-found error, and lets the final handler serialize it. Keep route registration and error handling in one application setup module when the service has several routers. That makes the order visible during review.
If you use a package, add it through the project’s existing dependency manager and import it using the module system already configured. The examples focus on API usage rather than installation. Add a dependency through the package manager already used by the project, following its manifest and lockfile.
Note: keep protocol status and application errors separate
A status code answers a protocol-level question about the request. An application error key answers which domain rule failed. For example, two different validation failures may both use 422 while returning distinct keys such as EMAIL_INVALID and ADDRESS_REQUIRED.
Do not make clients parse a human sentence to decide what to do next. Preserve stable keys across releases, document whether a field is optional or retryable, and keep translated or edited message text separate from program logic. That division lets the response stay readable without making a wording change an API breaking change.
For every response, check the status, Content-Type, and body together. A 201 with no indication of what was created may leave a client guessing, while a 204 followed by JSON contradicts the status semantics. An error response with a 200 status has the same mismatch in the other direction.
Standard codes to keep close at hand
The standard registry contains more codes than a typical API needs to choose directly. Start with the codes that express common resource operations and failure classes, then add another code only when its semantics match the situation and the client has a documented use for it.
| Group | Code | Meaning in an API |
|---|---|---|
| Success | 200 | Completed request with a representation |
| Success | 201 | Resource created |
| Success | 202 | Accepted for later processing |
| Success | 204 | Completed with no response content |
| Client | 400 | Request syntax or framing is invalid |
| Client | 401 | Authentication is missing or invalid |
| Client | 403 | Caller lacks permission |
| Client | 404 | Resource or route is unavailable |
| Client | 409 | Current resource state conflicts with request |
| Client | 422 | Content is understood but fails validation |
| Client | 429 | Rate limit applies |
| Server | 500 | Unexpected server failure |
| Server | 502 / 504 | Gateway received a bad upstream response or timed out |
| Server | 503 | Service is temporarily unavailable |
This compact selection is a starting point, not a replacement for the IANA registry or an API contract. Check the authoritative status reference when an endpoint needs a less common code, and document the response shape so callers know what the code means in this application.
Choose a code that describes the operation’s result. A successful POST that creates a user returns 201, while a successful read usually returns 200. A 202 means processing has been accepted and remains underway, so it suits a queued job.
A 204 signals success with no content, such as a delete whose caller needs no representation. Keep the response body consistent with that choice so clients do not try to parse content that the status says is absent.
For client errors, distinguish malformed requests from valid syntax that fails a rule. Malformed JSON can return 400, while parsed input that fails semantic validation can return 422.
Use 409 when a valid operation conflicts with current state, such as reserving a slot another request already took. Document the policy so clients do not infer different rules from each endpoint.
Authentication establishes who is calling, while authorization checks what that caller may do. A 401 requests valid authentication. A 403 identifies an authenticated caller who lacks permission.
An API may return 404 instead when revealing a protected object would expose sensitive information. Treat that as a deliberate access-control policy, not an accidental substitution.
The status code does not define the full error format. Return a stable error key for program logic, plus explanatory text and safe field details when useful.
Keep client decisions tied to the stable key rather than the message. Wording can change as the product improves, but a changed key can break callers.
Reserve 5xx responses for failures the server could not fulfill. A 500 indicates an unexpected condition. A 502 describes an invalid upstream response from a gateway, and 504 means that gateway timed out.
A 503 signals temporary unavailability. Add retry guidance only when the service can provide a useful estimate, since an inaccurate delay can make client behavior worse.
Redirect status codes differ in whether clients retain the request method. A 301 or 302 can cause some clients to turn POST into GET, while 307 and 308 preserve the method and body.
For an API, redirect only when clients should follow a stable canonical endpoint. Test the clients your service supports before changing this behavior.
For 429, the client may be able to retry after a delay. Include Retry-After only when the server has a specific delay or recovery time to communicate. For 503, that header can describe expected service recovery, but leaving it out is safer than inventing an estimate.
Monitoring also benefits from meaningful distinctions. A rise in 4xx responses can point to client input or authentication problems, while 5xx responses usually warrant server-side investigation. Treat these as signals rather than a complete diagnosis, because a particular API may have documented exceptions.
Informational responses
Informational 1xx responses describe progress or connection setup. They are interim messages, so a client still expects a final response for the request.
For ordinary JSON endpoints, application code rarely selects a 1xx code as its final result. HTTP/1.1 100 Continue lets a server indicate that a client may send a request body after checking its headers. A client should not treat it as completed work.
HTTP 101 Switching Protocols is used when the parties agree to change protocols, such as an HTTP connection upgrading to WebSocket. That exchange belongs to connection handling rather than the usual response returned by a route handler.
Redirection messages
A redirect response points the client to another location through the Location header. The status determines how the follow-up request should behave.
| Status | Meaning for a client | Typical use |
|---|---|---|
| 301 Moved Permanently | The resource has a permanent new location | Canonical URL migration |
| 302 Found | The resource is temporarily available elsewhere | Temporary browser navigation |
| 303 See Other | Retrieve another resource, commonly with GET | Post/Redirect/Get flow |
| 307 Temporary Redirect | Repeat the request with the same method | Temporary endpoint relocation |
| 308 Permanent Redirect | Repeat the request with the same method at a new location | Permanent endpoint relocation |
Pick the redirect by considering permanence and whether the method and body must be retained. A browser navigation and an API client may handle redirects differently, so test the clients your service supports before changing a redirect response.
Browser compatibility and client behavior
HTTP status meanings are protocol semantics, while the behavior after receiving them belongs to the client. Browsers may render their own page for an error response, JavaScript clients may expose a rejected fetch operation, and command-line clients may follow redirects according to their options.
Fetch resolves its promise when an HTTP response arrives, including responses such as 404 or 500. Check response.ok or response.status before treating the response body as successful application data. Network failures and blocked requests follow a different error path because there is no accessible HTTP response to inspect.
For a browser-based client, verify the status and body in the Network panel. Confirm that the request reached the intended endpoint, inspect any redirect chain, and check the final response headers. This separates an HTTP error from a client-side exception or a failed network request.
Cross-origin requests add another constraint. A browser enforces CORS rules based on response headers, and some requests trigger a preflight OPTIONS exchange before the actual method. A CORS failure can prevent JavaScript from reading a response even when the server returned a status, so inspect the browser console and network entry as well as the API logs.
Cache and retry implications
Some responses can be cached, and status selection affects how intermediaries interpret a result. A 404 may be cached under HTTP caching rules unless response headers or application policy prevent it. If a resource is created moments later, an old cached not-found response can confuse clients.
For mutable or authorization-sensitive resources, choose cache-control headers deliberately. Status codes alone do not express whether a particular user may reuse a response or how long a result remains fresh.
Retries need a similar explicit policy. A client might retry a network timeout or a temporary 503, but repeating a non-idempotent request can create duplicate work if the server completed it and the response was lost. Use an idempotency key or a request identifier when the operation needs safe retry behavior.
A 429 response can include Retry-After to communicate a delay, while a temporary service outage may use the same header when a recovery estimate is available. Clients should apply backoff and avoid synchronized retry loops. The server should send a value only when it has a meaningful policy to communicate.
Last Updated
HTTP behavior depends on the protocol specification and the client implementation. When maintaining this guidance, check the linked MDN status reference and the Express error-handling documentation for changes to terminology and framework behavior.
How to handle HTTP status codes in Express
Express lets a route set the status directly, or pass an error to error-handling middleware. Use direct responses for a route-specific result. Use middleware when routes should share an error format.
Step 1: Return an explicit status from a route
Chain res.status() before json() so the response carries both the HTTP result and a predictable body. This example is ordinary Express route code and does not require a status-code package.
app.post('/users', async (req, res, next) => {
try {
const user = await createUser(req.body);
return res.status(201).json({ id: user.id, name: user.name });
} catch (err) {
return next(err);
}
});
app.delete('/users/:id', async (req, res, next) => {
try {
await deleteUser(req.params.id);
return res.status(204).end();
} catch (err) {
return next(err);
}
});The create route returns 201 because it creates a resource. The delete route ends with 204 because it has no representation to send. Returning after each response prevents later statements from trying to write to a completed response.
Step 2: Pass known HTTP errors through middleware
Boom creates HTTP-friendly error objects with a status and formatted payload. The maintained package is @hapi/boom. Its error helpers are useful when service or route code should signal an HTTP outcome without writing the response itself.
import Boom from '@hapi/boom';
app.get('/users/:id', async (req, res, next) => {
try {
const user = await findUser(req.params.id);
if (!user) {
throw Boom.notFound('User not found');
}
return res.json(user);
} catch (err) {
return next(err);
}
});
app.use((err, req, res, next) => {
if (res.headersSent) {
return next(err);
}
if (Boom.isBoom(err)) {
return res
.status(err.output.statusCode)
.json(err.output.payload);
}
console.error(err);
return res.status(500).json({
error: 'INTERNAL_ERROR',
message: 'An unexpected error occurred'
});
});Mount the error handler after the routes. It sends Boom’s status and payload for expected HTTP errors, then maps unknown failures to a generic 500 response. The log keeps the original exception for diagnosis, while the response avoids exposing its stack or internal message.
Step 3: Use named constants when they clarify intent
The http-status package maps names such as NOT_FOUND and CREATED to numeric status codes. It does not create errors or send responses, so pair its constant with Express response methods.
import { status } from 'http-status';
app.get('/reports/:id', async (req, res, next) => {
try {
const report = await findReport(req.params.id);
if (!report) {
return res.status(status.NOT_FOUND).json({
error: 'REPORT_NOT_FOUND',
message: 'The requested report was not found'
});
}
return res.status(status.OK).json(report);
} catch (err) {
return next(err);
}
});The package documentation supports named ESM imports and CommonJS exports. Match the import form to the module system already used by your application rather than changing the project configuration for this one constant.
Status handling edge cases to check
The status is only useful if the server can still write the response and the body matches the status. Check asynchronous error flow, already-sent headers, and sensitive server failures before relying on a shared handler.
| Case | Symptom | Fix |
|---|---|---|
| Rejected async work | A promise rejects without reaching the error handler. | Express 5 forwards rejected handler promises. In Express 4, catch and pass the error with next(err). |
| Headers already sent | A second response triggers a headers-sent error during streaming. | Check res.headersSent and delegate the error. The default handler may close the connection. |
| Unexpected exception | Implementation details could leak through an error response. | Log the exception on the server and return a generic 500 with a stable error key. |
| Validation failure | Clients cannot tell malformed syntax from invalid field values. | Choose 400 for malformed input and 422 for parsed data that fails validation, then apply the policy consistently. |
| Rate limit or outage | Clients retry too soon or treat a temporary outage as permanent. | Return 429 for a rate limit and 503 for temporary unavailability. Include retry information only when known. |
| Empty success | A 204 response is paired with JSON content. | End the response without a body. |
When a browser and a server appear to disagree, compare the request URL, method, response status, response headers and response body. A redirect may mean the browser displays a different endpoint than the one the client initially called.
Check whether an intermediary changed the result. A reverse proxy can generate a gateway error before Express receives the request, and a CDN may return a cached response. Correlate the request identifier across proxy and application logs before changing route logic.
For an API test, record the method and status alongside the response payload. A useful failure report names the expected status, actual status and stable error identifier. That evidence distinguishes a routing mistake from a validation rule or an unexpected server exception.
Keep validation errors actionable but bounded. Return field names and safe constraints that help a caller correct a request. Avoid echoing submitted secrets, raw database messages or internal paths in the response, even when those values would help reproduce the failure.
Clients also need a policy for empty bodies. A 204 response has no content to parse, so client code should not call JSON parsing unconditionally. When an endpoint can return either a representation or an empty response, branch on the status before selecting a body parser.
Validation policy should be explicit in the API contract. A parser can reject malformed JSON with 400 before route validation begins, while a well-formed object with a missing required field can receive 422 under the convention used here. Clients can then distinguish a request they must repair syntactically from one whose values need correction.
For an unexpected error, log a correlation identifier and return a generic public message. An operator can use the identifier to find diagnostics without exposing stack traces or infrastructure data.
Review a successful request, an expected client error and an unexpected exception. Check status and JSON separately, then confirm the unknown-error response contains no exception text.
Conclusion: keep status and error body aligned
Express already handles direct status responses well. Add Boom when HTTP-aware errors need to pass between layers, and add http-status when named constants clarify intent. Whichever route you use, keep status semantics consistent and keep internal failure details out of public responses.
Use this compact check when response behavior looks wrong:
| Case | Symptom | Fix |
|---|---|---|
| Redirect | POST arrives as GET at the target | Use 307 or 308 when the method and body must remain intact |
| Empty success | Client JSON parsing fails on 204 | Skip body parsing for 204 or return a representation with 200 |
| Unexpected failure | Response exposes internal details | Return generic 500 content and log a correlation identifier |
For protocol definitions, see MDN’s HTTP status code reference. Our Express request guide covers route flow, and our headers-sent guide explains the streaming boundary.
For middleware behavior, consult the Express error-handling guide. Check the Boom API and http-status package documentation for library details.
FAQ
These short answers cover the choices that come up when adding status handling to an Express API.
Should I use Boom or Express res.status()?
Use res.status() for route-specific responses. Boom helps when HTTP-aware errors need to pass through shared middleware or service layers.
Does http-status send an HTTP response?
No. It provides named status-code constants. Use the numeric value with Express response methods such as res.status().json().
What should an API return for an unexpected error?
Return a generic 500 response and log diagnostic details on the server. Do not expose stack traces or sensitive exception messages.




