Logs are for machines first
console.log("something broke??") is fine while learning. In production, logs are searched, by tools, across millions of lines. So services write structured logs: one JSON object per line with consistent fields.
{"level":"info","message":"request handled","method":"GET","path":"/users","status":200,"ms":12}Now "show every request slower than 500 ms" or "count 500s by path" is a query, not an archaeology dig.
Each line carries a level: debug (development detail), info (normal events), warn (odd but survivable), error (a thing failed). Production usually records info and up, which is why LOG_LEVEL sat in the config you just built.
Code exercise · javascript
Run this micro-logger. The spread (...fields) merges extra fields into the log object, giving every line the same base shape.
One place to catch everything
Errors will happen: bugs, dead databases, weird input nobody imagined. The question is what leaves the building. Two rules:
- Clients get the stable error shape from lesson 4-3, never a stack trace, the multi-line dump listing every function call and file path on the way to the crash. Stack traces reveal file paths, library versions, and sometimes secrets.
- Errors are handled in ONE place, not per-route. In Express that is the error-handling middleware, recognized by its four parameters:
app.use((err, req, res, next) => { logger.error({ message: err.message, path: req.url }); const { status, body } = toResponse(err); res.status(status).json(body); });
toResponse translates errors for the outside world: known error codes map to their status and message, everything unexpected collapses to a generic 500. You write it next.
Quiz
An unexpected error (a bug) throws while handling a request. What should the CLIENT receive?
Code exercise · javascript
Your turn. Complete toResponse(err). Known codes (in KNOWN) map to their status and keep the message. Anything else becomes 500 with code "internal_error" and the message "something went wrong", hiding the real message from clients.