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:
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 Twitter • Edit on GitHub