Power up your Node.js debugging and error handling with the new Error cause feature
A new feature named “Error cause” was recently introduced in Node.js and in browsers. The feature allows you to chain errors like this:
This feature already exists in other languages and platforms like Java, Python or .NET under various names : Inner Exception, Nested Exception, Error origin, Error from, Exception chaining, etc.
If you’re a software engineer like me, I don’t need to tell you how hard debugging can be.
More often than not, the most cumbersome bugs are the ones where we don’t have enough context:
- 🤔 Why do we have a “Request failed” while trying to call this external API ?
- 🤔 Why did our microservice throw a “Failed to save entity” error in this particular case ?
- 🤔 What does this error message mean ?
Generic errors like these certainly don’t help us in understanding, reproducing or fixing failures in our software.
In this article, I aim to provide you with a way to avoid these kinds of situations at little to no cost, thanks to a new feature proposed by the TC39 committee.
To illustrate this feature, we will consider a very simple example, where we try to fetch two objects, foo and bar, from an API.
We see a big problem with our error handling: It is difficult, when the apiFetch function throws an error, to identify the step at which the error occurred. Were we fetching foo or bar at the time of the error? Indeed, our error lacks context.
To remedy this, we can try to throw an error a bit higher so we can identify the step at which the error occurred:
We added context to our error, but we also lost something important!
When the fetch function throws an error, the error contains details such as: request payload, HTTP status, error code returned by the API… (the details depends on the http client library you’re using)
By catching this error and rethrowing a brand new generic one (“An error has occurred while X”), we lose all of this precious information. Indeed, our error lacks content.
The old solution
We could try to instantiate the error like this:
But this approach has its drawbacks:
- 😕 Only the error message of apiError is added to the newly constructed Error ! Among potentially other things, we won’t have access to the stack trace (loss of information)
- 😕 When having more than two errors, it can be hard to identify where one error starts and the other ends (decreased readability of logs)
- 😕 This doesn’t explicit the intent of our code, which is to express that the new Error is caused by the apiError we’re catching (room for improvement in the readability of our code)
The new solution
Thankfully, Starting with node 16.9.0, we can add a cause to our error constructor, like so:
This way, we can build causality links between our errors !
Thanks to this feature, at each step of our code that can go wrong (ex: fetching foo), we’ll catch errors thrown by functions below, throw an error that specifies context (ex: An error has occurred while trying to fetch foo) together with the content of the error (ex: 404 — no such object found).
Starting with node 17.3.0, logging an error also logs all its causes recursively.
So instead of the error looking like this:
or like this:
It will now look like this :
We can add as many error levels as we like:
Our logs and errors are now much clearer and contain much more context !
For TS users, please keep in mind that the typings for this feature are not yet ready (as of Typescript 4.5.4).
However, with the help of a temporary //@ts-ignore, the feature can still be used in the meanwhile:
While trying out or implementing this feature, don’t forget to use the appropriate node version 😉. I personally use nvm to easily switch between node versions.
I’ve mentioned that the feature is available in node 16.9.0 (for cause constructor only) but it is also available to use in your frontend code, for Chrome (93+), firefox (91+) and Safari (15+) ! Logging the error cause is not available on all platforms but it’ll surely be soon enough :)
For older versions, you can also use polyfills (see the repository of the proposal for more details)
- Debugging errors is easier when having immediate access to both error context and error details
- One way to achieve this is to apply a “catch + rethrow with context” pattern, using the new error cause feature:
- This way, we can have more complete and easily actionable logs
- This feature is brand new, so don’t be surprised if not yet 100% operational on your platform
Happy debugging ! 😊