Explain Codes LogoExplain Codes Logo

Is it not possible to stringify an Error using JSON.stringify?

javascript
error-handling
json-stringify
error-serialization
Nikita BarsukovbyNikita Barsukov·Oct 14, 2024
TLDR

JSON.stringify() fails to stringify Error objects due to their non-enumerable properties. Use the following workaround:

const error = new Error('Oops'); const serializedError = JSON.stringify( error, Object.getOwnPropertyNames(error) );

Object.getOwnPropertyNames() ensures a complete serialization of the Error object by grabbing all properties.

Understanding error serialization

The hurdle to stringify an Error object stems from the non-enumerable properties of an error object which JSON.stringify() overlooks. Here are several strategies to handle this issue effectively.

Enhancing error serialization with toJSON

One approach is adding a toJSON method to Error.prototype, to prepare Error objects for serialization:

if (!('toJSON' in Error.prototype)) { Object.defineProperty(Error.prototype, 'toJSON', { value: function () { const alt = {}; Object.getOwnPropertyNames(this).forEach(function (key) { alt[key] = this[key]; // "Stealing" properties, one by one }, this); return alt; }, configurable: true, writable: true }); }

With this enhancement, Error objects now are exposed to JSON.stringify and get successfully serialized.

Using a replacer function

Alternatively, we can customize the serialization with a replacer function in JSON.stringify:

function replaceErrors(key, value) { if (value instanceof Error) { let error = {}; Object.getOwnPropertyNames(value).forEach(function (key) { error[key] = value[key]; // "Relocating" property ownership }); return error; } return value; } const error = new Error('Something went wrong'); const serializedError = JSON.stringify( error, replaceErrors );

This technique bypasses the non-enumerability of Error properties, enabling a wholistic serialization.

Creating enriched error objects

Personalized error objects offer an avenue for adding enumerated properties. These custom errors can combine useful details right out of the box when stringified:

class DetailedError extends Error { constructor(message, ...params) { super(message, ...params); if (Error.captureStackTrace) { Error.captureStackTrace(this, DetailedError); } this.name = 'DetailedError'; this.date = new Date(); this.customProperty = 'Custom value'; // Anything you fancy } } const detailedError = new DetailedError('Detailed info'); const serializedDetailedError = JSON.stringify(detailedError);

Utilizing external libraries

In case you prefer a ready-made solution, the serialize-error npm package offers serialization for error objects right off the shelf:

const { serializeError } = require('serialize-error'); const error = new Error('Example'); const serializedError = JSON.stringify(serializeError(error)); // Voila! Non-standard, but brilliantly handled.

Visualization

A simple JSON.stringify() call seems to comprehensively transform complex objects into JSON strings:

JSON.stringify({ ... }) // Creates a clear JSON string

But when we attempt the same process with an Error object:

let error = new Error("Mysterious problem"); JSON.stringify(error); // Returns "{}"

It's a head-scratcher, akin to this equation:

📸 + 👻 = ❓

The details (stack trace, message) are invisible to the typical operation by JSON.stringify, as it misses the non-enumerable properties.

Unraveling the ghost properties

The core of the issue involves enumerable vs non-enumerable properties. Error properties are non-enumerable by design, hypothetically to avoid unwittingly revealing sensitive data.

Applying selective serialization

It may be the case that you only wish for particular properties to be serialized. Here, a custom filter function can deliver granular control:

const error = new Error('Selective serialization'); const filteredSerializedError = JSON.stringify( error, (key, value) => ['name', 'message', 'stack'].includes(key) ? value : undefined );

This function only stringifies the name, message, and stack properties, quietly disregarding incidental or sensitive records.

Ensuring serialization sanity

Before deploying any serialization strategy, it's crucial to test extensively. Verification of your stringifyError functions and the richness of your error logs forms the cornerstone of tough and reliable error handling.