Logging errors in Node.js correctly
If you are using a structured logging format like JSON or logfmt with Node.js logging libraries, then you will have likely seen error log lines like the following in your observability tool:
{ "level": 50, "time": 1738381994206, "pid": 38355, "hostname": "Sophias-MacBook-Pro-3.local", "error": {}, "msg": "Failed to poll for job completion"}
The empty error object occurs when you try to log an Error object like so:
try { const result = await pollJobForCompletion(jobId); // ...} catch (error) { if (error instanceof CouldNotPoll) { logger.error({ error }, 'Failed to poll for job completion'); return; } // ...}
The root cause of the problem is that all of the interesting fields on an Error object are not enumerable, and the standard formatting libraries format objects by looping over their enumerable keys. You can verify this by running the following code in a REPL:
Object.keys(new Error()) // => []In pino you can solve the problem by installing the excellent pino-std-serializers package and adding errWithCause to your serializers map. This assumes you always log errors under the error key, as pino does not run serializers against the entire log splat for performance reasons:
import { errWithCause } from 'pino-std-serializers';import pino from 'pino'; export const logger = pino({ // ... serializers: { error: errWithCause, },})
If you are using winston then you can solve the problem by adding your own custom formatter (there's a winston.format.errors API but I had problems with it in the past alongside the JSON formatter—but maybe it is better now):
import * as winston from 'winston'; const errorFormatter = winston.format((info) => { // assumption: you always log errors under the `error` key if ('error' in info && info.error instanceof Error) { // if you're worried about mutation, create an empty object // and copy over enumerable properties + these special ones Object.defineProperty(info.error, 'message', { enumerable: true, value: info.error.message, }); Object.defineProperty(info.error, 'name', { enumerable: true, value: info.error.name, }); Object.defineProperty(info.error, 'stack', { enumerable: true, value: info.error.stack, }); } return info;}); export const logger = winston.createLogger({ // ... formatter: winston.format.combine( winston.format.timestamp(), errorFormatter, winston.format.json(), ),});
After doing this your log lines will be useful again:
{ "level": 50, "time": 1738383562543, "pid": 43552, "hostname": "Sophias-MacBook-Pro-3.local", "error": { "type": "Error", "message": "some error occurred", "stack": "Error: some error occurred\n at REPL22:1:20\n at ContextifyScript.runInThisContext (node:vm:137:12)\n at REPLServer.defaultEval (node:repl:598:22)\n at bound (node:domain:432:15)\n at REPLServer.runBound [as eval] (node:domain:443:12)\n at REPLServer.onLine (node:repl:927:10)\n at REPLServer.emit (node:events:531:35)\n at REPLServer.emit (node:domain:488:12)\n at [_onLine] [as _onLine] (node:internal/readline/interface:415:12)\n at [_line] [as _line] (node:internal/readline/interface:886:18)" }, "msg": "Failed to poll for job completion",}