Skip to content

Beginner · APIs and request handling · topic 11 of 54 · 25 min

Handling errors well

Explanation

Things go wrong: bad input, missing records, a crashed database. Good backends answer every error with the right status code and the same JSON shape, like { error: { code, message } }. Unexpected crashes are caught, logged in full on the server, and answered with a plain 500 that never shows the real error to the user.

Two kinds of errors

An expected error is one you plan for: a missing product (404), a wrong field (400 or 422), a user who is not logged in (401). An unexpected error is a surprise: a bug, a typo, or a database that is down. Both need a clear response, but they are handled differently.

One error shape for the whole API

Every error response should have the same shape, so the app can show errors with one piece of code. A common shape is an error object with a code (a fixed word in capitals that programs can check) and a message (a sentence for people).

json
{
  "error": {
    "code": "NOT_FOUND",
    "message": "Product 57 was not found"
  }
}
The same shape for every error: a code for programs, a message for people.
javascript
function errorResponse(status, code, message) {
  return { status, body: { error: { code, message } } };
}

console.log(errorResponse(404, 'NOT_FOUND', 'Product 57 was not found'));
console.log(errorResponse(401, 'NOT_LOGGED_IN', 'Please log in first'));
A small helper keeps every error response the same.

Catching unexpected errors

If a handler throws an error and nobody catches it, the request may hang or the whole server may crash. So we wrap every handler with try and catch. In the catch block we log the real error for developers and send a plain 500 to the user.

Think of it like this: Think of a bank's customer care call. When their system fails, the agent says "Sorry, we have a technical problem. Your reference number is 4471." They do not read out the internal error screen to you. The engineers look up reference 4471 in their own logs.
javascript
class HttpError extends Error {
  constructor(status, code, message) {
    super(message);
    this.status = status;
    this.code = code;
  }
}

function safe(handler) {
  return (request) => {
    try {
      return handler(request);
    } catch (error) {
      if (error instanceof HttpError) {
        return { status: error.status, body: { error: { code: error.code, message: error.message } } };
      }
      console.log('LOG for developers:', error.message); // the real reason stays on the server
      return { status: 500, body: { error: { code: 'INTERNAL', message: 'Something went wrong. Please try again.' } } };
    }
  };
}

const getOrder = safe((req) => {
  if (req.id === 7) throw new HttpError(404, 'NOT_FOUND', 'Order 7 was not found');
  if (req.id === 8) throw new Error('database password rejected');
  return { status: 200, body: { id: req.id } };
});

console.log(getOrder({ id: 1 }).status); // 200
console.log(getOrder({ id: 7 }).status); // 404 with a clear message
console.log(getOrder({ id: 8 }).body);   // plain 500 message, no password text
Expected errors (HttpError) keep their status. Anything else becomes a plain 500, and the details are only logged.
Careful: Never send error.stack, SQL error text or file paths to the client. They tell attackers your table names, folder layout and library versions. Log them on the server; send a plain message to the user.

The not-found handler

At the end of the route table, add one last handler that catches every request no other route matched. It returns 404 with the same error shape, like { error: { code: "NOT_FOUND", message: "No route for GET /prodcts" } }. This is much better than an empty page or a crash.

Tip: Give every request an id (a random string) and put it in both the log line and the error message, like "Reference: a1b2c3". When a user complains, you can find the exact log line in seconds.

Watch the lesson

Handling errors well · 1/6

Clear for users Safe from attackers

Errors will happen. A good backend answers every error clearly and safely.

Prefer a video by a teacher? Find free videos on “error handling in node js express api best practices” on YouTube ↗

In real life: A train booking at peak time

At 10 a.m. Tatkal time, thousands of people book at once. The database gets slow and one query times out. The booking backend catches the error, writes the full details with request id t-90211 into its log, and answers 503 with {"error": {"code": "BUSY", "message": "Too many bookings right now. Please try again. Reference t-90211"}}. The user sees a clear message, and the engineers see the real cause in the logs.

Practice (0/3 done)

Task 1 · Code · Javascript

Write a function errorResponse(status, code, message) that returns { status, body: { error: { code, message } } }.

Task 2 · Code · Javascript

Write a function withErrors(handler) that returns a new function (request) => response. It calls handler(request) and returns its response. If the handler throws, it logs the error with console.error and returns { status: 500, body: { error: { code: "INTERNAL", message: "Something went wrong" } } }. The real error text must never be in the response.

Task 3 · Code · Javascript

Write a function validationToResponse(errors) that takes a list of validation errors like [{ field: "mobile", message: "..." }]. If the list is empty, return null. Otherwise return { status: 400, body: { error: { code: "VALIDATION_FAILED", message: "Some fields are not valid", fields: errors } } }.

Quick quiz

Question 1

1.Why use the same error shape across the whole API?

Question 2

2.Your database throws "relation users_old does not exist". What should the client get?

Question 3

3.What happens if a handler throws and nothing catches it?

Question 4

4.Where does the not-found handler go in the route table?

Question 5

5.Why put a request id in both the log and the error message?

Read more (free)

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

← Previous: Validating input