blog.larah.me

Don't rethrow `new Error(error)`!

April 29, 2020

You may be tempted to rethrow an error object passed to you by a library or function call.

Here’s an example using Apollo’s useQuery hook:

function Pets() {
  const { loading, error, data } = useQuery(GET_MY_PETS);

  if (loading) {
    return <PetListShimmerState />
  }

  if (error) {
    // let error bubble up and be caught by error handler somewhere else
    throw new Error(error);
  }

  return (
    <ul>
      <li key={pet.id}>{pet.name}: {pet.breed}</li>
    </ul>
  );
}

Or even more generically:

foo((err, data) => {
  if (err) {
    throw new Error(err);
  }
})

At first glance this seems innocuous. We’re being responsible; writing guards that catch errors and throw them, before continuing with our application logic.

(Hopefully this bubbles up to an error handler defined somewhere else in the application, which would ultimately deal with the errors!)

So what’s wrong with this?

Here’s the line we’re particularly interested in:

throw new Error(error);

new Error as you might expect creates a new instance of an error object. But in the useQuery example, the error variable is already an instance of Error!

This is a problem because valuable stack trace information (and any custom error object attributes) will be thrown away and lost when we create a brand new error object.

Specifically, it will hide where the error was originally thrown from in the stack trace. When you go to look at the stack traces you’ve collected, you’ll see that they start from where you re-threw the error - not where the underlying error occurred.

What should I do instead?

If you know that error is already an error object, you can simply rethrow it:

 foo((err, data) => {
   if (err) {
-    throw new Error(err);
+    throw err;
   }
 })

If you don’t know the type or provenance of the error variable (maybe it’s actually a string from a rejected promise), you can wrap the variable with ensure-error before throwing:

 foo((err, data) => {
   if (err) {
-    throw new Error(err);
+    throw ensureError(err);
   }
 })

(ensureError comes from https://github.com/sindresorhus/ensure-error)

Reproduction

Here’s a runnable example so you can see the difference between these two approaches:

https://repl.it/repls/LowestFlatPixels

Bonus: What if I do want to create a new error?

This is a totally legitimate thing to want to do! Maybe you have a custom error class that you want to throw instead, so you can provide better error messages.

(This is a good way to include the extra context of where the error is being rethrown from.)

It’s still recommended to include the original error, so you get the best of both worlds. The following libraries help you to do that:

Further Reading

Discuss on TwitterEdit on GitHub