PLAINFINCH learn by building

Logging and debugging

You will be able to use your server's log to find bugs, follow single requests and keep personal data private.

What is the Log for?

A server has no screen. When something goes wrong at the bike shop at closing time, nobody is watching the code run. What is left afterwards is the log: the lines the server printed with console.log and console.error, plus the errors Node.js and Express print for you. Logging is writing those lines on purpose, so the server can explain itself later.

The Log view shows everything printed in this run, tagged with the request that was being handled. The startup message comes first. Each request then adds whatever your code printed while handling it. When a handler throws, Express prints the error with the line number in server.js and answers 500. On a real server the same lines go to the terminal, or to the log page of your hosting platform.

How do you find a bug from a 500?

Press **GET /repairs/1**: it works. Press **GET /repairs/3**: 500 Internal Server Error. Don't start changing code yet. Open the Log and read the error. It says TypeError: Cannot read properties of undefined (reading 'join') and names a line. That is two facts: something on that line was undefined, and the code tried to call .join on it.

Now compare. Repairs 1 and 2 have a parts array. Repair 3 was just booked in and has none, so repair.parts is undefined. The route's code assumed that every repair has parts. Decide what should happen instead, here an empty parts list, and write that: (repair.parts || []).join(', '). Send the same request again and check the 200.

That loop works for almost every server bug: reproduce it with one request, read the error and its line, form a guess, add a console.log of the value you are unsure about if the error alone doesn't tell you, fix, and send the request again. Remove the temporary logging when you are done.

  • Reproduce: find the one request that fails, and keep using it.
  • Read: the error's name, its message and its line in server.js.
  • Look: print the values on that line if you can't see the problem yet.
  • Fix, then resend the failing request and one that worked before.

What makes a log line useful?

A good line answers when, which request, and what happened. The method and path say what was asked. The status and time taken say how it went, and a slow 200 is often the first sign of trouble. An id ties together every line from one request, which helps as soon as two requests are handled at once and their lines interleave.

Real servers usually write the request line when the response has finished, so it can include the status and duration. In Express that is done with res.on('finish', …) inside a middleware. The task logs at the start of each request to keep the first version short; the version below logs when the response finishes, and it works here and on your own computer. Use console.error for errors: it writes to a separate stream that hosts often highlight or alert on.

app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    console.log(`[${req.id}] ${req.method} ${req.originalUrl} ${res.statusCode} ${Date.now() - start}ms`);
  });
  next();
});

Why give each request an id?

A busy server writes thousands of lines an hour. When a customer says "booking failed at 5 o'clock", you need to find their request among all the others. A request id solves that. A middleware at the top makes one with crypto.randomUUID(), stores it on req.id, puts it in every line it logs, and sends it back in an X-Request-Id header.

Because the id is in the response, your error page can show it: "Something went wrong. Reference: 3f9c…". The customer reads it out, and you search the logs for exactly that request. If your host's proxy already adds a request id header, you can reuse it, but treat it like any other input: check it looks like an id before trusting it, and make your own when it doesn't.

FigureA middleware gives GET /repairs/3 the id 3f9c2a7e from crypto.randomUUID. The same id appears in the X-Request-Id response header, on the error page as a reference, and at the start of every Log line for that request.A middleware gives GET /repairs/3 the id 3f9c2a7e from crypto.randomUUID. The same id appears in the X-Request-Id response header, on the error page as a reference, and at the start of every Log line for that request.
The id made at the top of the request travels with it: into the X-Request-Id header, into every Log line and onto the error page. When a customer reads out the reference, you search the Log for it and land on the TypeError straight away.

What should never go in a log?

Logs travel further than the database. They are copied to hosting dashboards, kept for weeks, searched by several people, and pasted into chat messages while debugging. Anything you log, you should be comfortable with all of those people seeing. That rules out passwords, session ids, cookies, Authorization headers, API keys and reset tokens, and it should rule out personal details such as phone numbers, addresses and full email addresses unless you have a clear reason to keep them.

The usual way this goes wrong is logging a whole object: console.log(req.body), console.log(req.headers), or the whole user record. It is quick, it works while you debug, and it leaks whatever those objects ever contain without any warning. Log the specific fields you need, such as the booking id and the bike. Privacy laws treat logs as stored personal data, and a leaked log is a leak like any other.

// Too much: phone number, name, everything the form sends
console.log('New booking', req.body);

// Enough to follow what happened
console.log(`[${req.id}] booking ${booking.id} for a ${bike}`);

What should visitors see when something breaks?

Visitors get a short, polite message and the right status code. The details, the error message, the stack trace and the request id, go to the log. A stack trace in a page tells strangers your file paths, which libraries you use and how your code is organised, all useful to someone looking for a way in. Express's own error page hides the stack when NODE_ENV is production; your custom error handler has to make the same choice itself.

In the error handler, log with console.error(err) and include req.id, then answer with 500 and a message that offers the id as a reference. Never send err.stack, and be careful with err.message too, which can contain database details.

What else helps when logs aren't enough?

For a bug that only appears with particular data, a debugger saves a lot of guessing. Run node --inspect server.js and connect from your browser's developer tools; you can pause on a line, look at every variable and step through the handler. The debugger; statement pauses there when a debugger is attached. Writing a small automated test that sends the failing request is the other strong tool, because it keeps the bug from coming back.

When an AI tool adds logging, read what each line prints. Watch for whole objects, headers and bodies being logged, error handlers that send err.stack or err.message to the visitor, loggers placed after the routes, and a missing next(). Then send a request with fake private data, such as a made-up phone number or token, and search the Log for it.

Field notes

  • The log is the record of what your server did when nobody was watching.
  • When you get a 500, read the error in the log before you change any code.
  • A request id lets you find one visitor's request among thousands of lines.
  • Keep passwords, tokens and personal details out of the log.

Easy to get wrong

req.body on a login route contains the password in plain text. Once it is in a log file, it is copied to every place the logs go. Log only the fields you need, and never secrets.

The stack trace belongs in the log, where you can read it. Visitors get a short message and a reference they can quote, which leads you to the full error.

If you change several things and the bug disappears, you don't know which change fixed it or whether another one broke something. One change, one request, then read the result.

Questions people ask

Should I remove console.log before going live?

Remove the temporary lines you added while chasing a bug. Keep deliberate ones: one line per request, warnings, and errors. Those are what you will read when something goes wrong on the live site.

What is structured logging?

Writing each line as JSON, such as {"id":"3f9c…","method":"GET","status":500}, instead of free text. Hosting dashboards can then filter by field. Logging libraries for Node.js do this and add levels like info, warn and error, but the rules about what to leave out are the same.

Why does the practice server show the error line but a real server shows a long stack?

A real stack trace lists every function call that led to the error, including many inside Express. The practice server shortens it to the line in server.js, which is the part you can change. On a real server, read the stack from the top until you reach the first line in your own file.

How long should logs be kept?

Only as long as you need them to fix problems and spot abuse, often a few weeks. Hosting platforms let you set this. The less you keep, the less can leak, which is one more reason to leave personal data out in the first place.

Keep going