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",}