NodeJSHTTP Status Codes

HTTP Status Codes

Every HTTP response carries a three-digit status code that tells the client how the request went — succeeded, redirected, client error, or server error. Using the right code is not pedantry: clients, browsers, caches, search engines, and monitoring tools all make decisions based on it. Returning 200 OK for an error (a depressingly common mistake) breaks every one of them.

The five classes

Range

Class

Meaning

1xx

Informational

Request received, continuing (rare)

2xx

Success

The request succeeded

3xx

Redirection

Further action needed (go elsewhere)

4xx

Client error

The client did something wrong

5xx

Server error

The server failed

The first digit is the whole story
You can reason about any unfamiliar code from its first digit: `2xx` = good, `3xx` = look elsewhere, `4xx` = "you messed up" (don't retry unchanged), `5xx` = "I messed up" (retrying may help). The 4xx/5xx split — *whose fault* it is — is the most consequential distinction in the whole scheme.
Setting the status in Node

JS
// Two equivalent ways:
res.statusCode = 201
res.end()

res.writeHead(201, { 'Content-Type': 'application/json' })
res.end(JSON.stringify(created))
The codes you'll actually use

Code

Name

When

200

OK

Standard success (GET, PUT, PATCH)

201

Created

Resource created (POST) — include Location header

204

No Content

Success, but nothing to return (DELETE, some PUTs)

301

Moved Permanently

Permanent redirect — caches/SEO update

302 / 307

Found / Temp Redirect

Temporary redirect

304

Not Modified

Cached copy is still valid (conditional GET)

400

Bad Request

Malformed/invalid request

401

Unauthorized

Not authenticated (no/invalid credentials)

403

Forbidden

Authenticated but not allowed

404

Not Found

Resource does not exist

409

Conflict

State conflict (e.g. duplicate, version mismatch)

422

Unprocessable Entity

Well-formed but semantically invalid (validation)

429

Too Many Requests

Rate limit exceeded

500

Internal Server Error

Unhandled server-side failure

502 / 503 / 504

Bad Gateway / Unavailable / Timeout

Upstream/overload/timeout

401 vs 403 — authentication vs authorization
401 means 'who are you?'; 403 means 'I know who you are, and no'
These are routinely swapped. **401 Unauthorized** actually means *unauthenticated* — you haven't proven who you are (missing/expired/invalid token), and re-authenticating could fix it. **403 Forbidden** means you *are* authenticated, but you lack permission for this action — re-authenticating won't help. Sending 401 when you mean 403 can trigger clients to pointlessly re-prompt for login.
The cardinal sin: 200 for errors
Never return `200 OK` with an error in the body
A handler that catches an error and responds `200` with `{ "error": "..." }` lies to everyone: monitoring shows zero errors, clients can't `if (res.ok)`, caches may store the failure, and retries never trigger. **Match the code to reality** — `400`/`422` for bad input, `404` for missing, `500` for server faults. The status code is the contract; the body is detail.

JS
// ✗ Wrong — masks the failure as success
res.writeHead(200).end(JSON.stringify({ error: 'user not found' }))

// ✓ Right — the code tells the truth
res.writeHead(404, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'user not found' }))
201 and 204 — success beyond 200
  • 201 Created for a successful POST that made a resource — include a Location header pointing to the new resource's URL.

  • 204 No Content when there is genuinely nothing to send back (a DELETE, or a PUT that returns no representation). With 204 you must not write a body.

A `204` response must have no body
`204 No Content` means exactly that — sending body data with it violates the spec and confuses clients. Just call `res.writeHead(204).end()` with no argument. Likewise, `304 Not Modified` carries no body.
4xx vs 5xx and retries

The client/server split drives retry behavior. A 4xx means the request itself is wrong — retrying it unchanged will fail again, so don't. A 5xx means a transient server problem — the same request might succeed on retry (ideally with backoff). Clients and proxies bake this in, so classify correctly:

JS
try {
  const result = await handle(req)
  res.writeHead(200, { 'Content-Type': 'application/json' })
  res.end(JSON.stringify(result))
} catch (err) {
  if (err.name === 'ValidationError') {
    res.writeHead(422).end(JSON.stringify({ error: err.message }))  // client's fault
  } else {
    console.error(err)                                              // log server faults
    res.writeHead(500).end(JSON.stringify({ error: 'Internal Server Error' }))
  }
}
Never leak internal error details in a 500 body
A `500` response should say something generic like "Internal Server Error" — **not** the exception message, stack trace, SQL, or file paths. Those reveal your internals to attackers and confuse users. Log the full error server-side; return a clean, generic message (optionally with a correlation ID) to the client.
Next
The metadata that travels with every request and response: [HTTP Headers](/nodejs/http-headers).