Using the Spread Operator with Error Objects

August 06, 2024 | 3 minutes
TL;DR: Properties on `Error` objects are not enumerable in some browsers, which means you can't use the spread operator (...) to copy their properties to other objects.

Since the release of ES6, the spread operator has made it easy to take the properties from one object and add them to a new object. We regularly see this pattern when combining objects in modern JavaScript.

I often use this pattern when working with objects and it generally just works. I recently discovered that's not the case for Error objects, though. The spread operator only works for certain types of object properties and not all environments treat JavaScript properties the same way.

1const person = {
2    name: 'Jane',
3    age: 30
4};
5
6const car = {
7    make: 'Toyota',
8    model: 'Camry'
9};
10
11const driver = {
12    ...person,
13    ...car
14};
15
16// Output of driver: { name: 'Jane', age: 30, make: 'Toyota', model: 'Camry' }

Which Properties Are Spreadable?

When the spread syntax is used on an object, it only enumerates (or lists) the object's own properties. This means that if you spread object A into object B, only the non-inherited properties on object A will be added to object B.

As an example, if we were to add a property to the Object prototype, create a new object from it, and then spread that object into another, the property from the prototype wouldn't be added to the second object.

1Object.prototype.title = "Title";
2
3const objectA = {
4    name: "Name"
5};
6
7const objectB = {
8    date: '2024-08-06',
9    ...objectA
10};
11
12// Output of objectB = { date: '2024-08-06', name: 'Name' }

Why Can't You Spread an Error Object?

An Error object in JavaScript is the base object for errors generated in an app. You can use it as is or extend it to add custom functionality. It has a few standard instance properties, such as the message property, as well as a few non-standard properties that are only implemented by specific browsers.

I was recently working on a UI where I wanted to take an Error object caught after a failed API request and spread it within a custom error object so that I could add additional data for my React error boundary to render alongside the error. My code looked something like this:

1const makeApiRequest = async () => {
2    const response = await fetch(path);
3
4    if (!response.ok) {
5        throw new Error('There was an error getting the data.');
6    }
7}
8
9const useMakeApiRequest = async () => {
10    return useQuery({
11        queryKey: 'key',
12        queryFn: makeApiRequest(),
13    })
14}
15
16/**
17 * Error shape: { message: string }
18 * CustomError shape: { message: string, context: { item: Item } }
19 */
20class CustomError extends Error {
21  constructor({ message, context }) {
22    super();
23    this.message = message;
24    this.context = context;
25  }
26}
27
28const Wrapper = (item) => {
29    const { data, error } = useMakeApiRequest();
30
31    if (error) {
32        throw new CustomError({
33            ...error, // an Error object
34            context: {
35                item,
36            }
37        })
38    }
39
40    return <Component />;
41}

In tracing the code in Chrome, the error was thrown correctly and then re-thrown as a CustomError. But when the CustomError was thrown to the error boundary, I discovered that even though both the Error and CustomError have a message property, the CustomError ended up with message being undefined.

The reason that the message property ends up being undefined after the Error object is spread into the CustomError object is that the message property on Error is not enumerable. This means that the message property cannot be copied to another object this way. Instead, we need to specifically take the message property from the Error and explicitly add it to the CustomError object like this:

1const Wrapper = (item) => {
2    const { data, error } = useMakeApiRequest();
3
4    if (error) {
5        const { message } = error;
6
7        throw new CustomError({
8            message: message,
9            context: {
10                item,
11            }
12        })
13    }
14
15    return <Component />;
16}

Some browsers implement enumerability differently, so you may find that sometimes the spread operator works with Error objects and other times it doesn't. At the time of writing, Error properties are not enumerable in Chrome.