Skip to main content

HTTP status codes

HTTP status codes are three-digit numbers a server puts in every HTTP response to tell the client exactly what happened to its request.

Theory

TL;DR

  • Think of them as mail delivery receipts: 200 = delivered, 404 = address doesn't exist, 500 = post office broke it
  • The first digit is the category: 2xx success, 3xx redirect, 4xx client mistake, 5xx server mistake
  • fetch() sets response.ok to true only for 2xx responses
  • 401 means not authenticated (prompt login), 403 means authenticated but no permission
  • Decision rule: 4xx means fix your request, 5xx means wait or alert the server team

Quick example

javascript
// Express.js - run with: npm init -y && npm i express && node server.js const express = require('express'); const app = express(); app.get('/', (req, res) => res.status(200).send('Success')); // 200 OK app.get('/missing', (req, res) => res.status(404).send('Not found')); // 404 Not Found app.get('/error', (req, res) => { throw new Error(); }); // 500 Internal Server Error app.listen(3000); // curl localhost:3000/ → 200 // curl localhost:3000/missing → 404 // curl localhost:3000/error → 500

The first argument to res.status() is the number Express writes into the response status line before flushing headers to the client.

The five categories

The HTTP spec groups all codes by their first digit:

  • 1xx (Informational) - server received the request and is still processing. Rare in practice. Example: 100 Continue.
  • 2xx (Success) - request worked. Use 200 for a standard OK, 201 when you created a resource, 202 when the job started but is not done yet.
  • 3xx (Redirect) - resource moved. 301 is permanent (update your bookmarks), 302 is temporary (keep checking the original URL).
  • 4xx (Client error) - something in the request was wrong. 400 bad input, 401 not authenticated, 403 authenticated but no access, 404 not found, 429 too many requests.
  • 5xx (Server error) - request was fine, server failed. 500 generic crash, 503 temporarily unavailable.

When to use

  • User submits valid data and a resource is created → 201 Created
  • Requested resource does not exist → 404 Not Found
  • Request has no valid token → 401 Unauthorized
  • Token is valid but role lacks permission → 403 Forbidden
  • Database crashed → 500 Internal Server Error
  • Planned maintenance → 503 Service Unavailable
  • Async job queued but not complete → 202 Accepted

Common mistakes

Returning 200 on validation errors. This is the most common mistake in junior APIs. The client gets a 200 and assumes data was saved. The database rejects it, nothing persists, and nobody knows why.

javascript
// Wrong - lying to the client app.post('/users', (req, res) => { if (!req.body.email) res.status(200).send('OK'); }); // Correct app.post('/users', (req, res) => { if (!req.body.email) return res.status(400).send('Email required'); });

Sending 500 for every error. If a user sends a bad auth token and you return 500, logs flood with false alarms and the frontend team has no idea the token is the problem. 4xx is for client mistakes, 5xx is for yours.

javascript
// Wrong app.use((err, req, res, next) => res.status(500).send('Error')); // Better app.use((err, req, res, next) => { if (err.name === 'ValidationError') return res.status(400).send(err.message); if (err.name === 'UnauthorizedError') return res.status(401).send('Invalid token'); res.status(500).send('Server error'); });

Ignoring 429 in fetch loops. Hit an API in a tight loop without checking for rate limiting and your IP gets blocked. Always check for 429 and back off.

javascript
if (res.status === 429) { await new Promise(r => setTimeout(r, 1000 * Math.pow(2, retryCount))); }

I watched a production integration go down because someone looped 1000 GitHub API calls without backoff. The IP was blocked in under 30 seconds.

Real-world usage

  • Express.js - res.status(201).json(user) after POST /users
  • React Query - if (!res.ok) throw new Error(res.status) inside queryFn
  • Next.js API routes - res.status(401).json({ error: 'Unauthorized' }) in middleware
  • Stripe - 200 on successful charge, 402 Payment Required on insufficient funds
  • GitHub API - 404 on non-existent repo, 422 on an invalid PR merge

Follow-up questions

Q: What is the difference between 401 and 403?
A: 401 means the request has no valid credentials - prompt the user to log in. 403 means credentials are valid but the user has no permission for that specific resource.

Q: When would you return 202 instead of 200?
A: Use 202 Accepted when the job is queued but not done yet - like triggering a report export or sending an email in the background. The client knows to poll or wait for a callback.

Q: What is the difference between 301 and 308?
A: Both are permanent redirects. But 301 allows the browser to change POST to GET on redirect, while 308 preserves the original method and body. Use 308 when POST data must survive the redirect.

Q: An API gateway is returning 502 to clients. What do you check first?
A: 502 Bad Gateway means the proxy got an invalid response from the upstream service. Check if upstream is running, review its logs, verify proxy timeout settings, and consider a circuit breaker so one failing service does not take down the whole chain.

Examples

Webhook validation with Stripe-style error handling

javascript
// POST /webhook - validate signature before processing payment event app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.headers['stripe-signature']; let event; try { event = stripe.webhooks.constructEvent(req.body, sig, process.env.ENDPOINT_SECRET); res.status(200).json({ received: true }); // 200: event processed successfully } catch (err) { res.status(400).send(`Signature check failed: ${err.message}`); // 400: bad client input } });

A valid webhook returns 200. An invalid signature returns 400, not 500 - the payload is the client's responsibility, not a server crash.

Handling status codes in fetch()

javascript
async function loadUser(id) { const res = await fetch(`/api/users/${id}`); if (res.status === 404) return null; // missing user is a valid state, not an error if (res.status === 401) { window.location.href = '/login'; // redirect to login return; } if (!res.ok) throw new Error(`Error: ${res.status}`); // 500s and unknowns return res.json(); }

Treating 404 as a normal state and 401 as a redirect trigger gives users a clean experience instead of a generic error message for every non-200 response.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?