Assumed audience: TypeScript and JavaScript developers who might not yet be aware of the Error.prototype.cause property and how to use it to get structured error chains.
Since September 2021, all modern browsers and server-side runtimes support a cause property on an Error. This is really handy when you have a lower-level error that you want to handle in some cases or bundle into a higher-level error in other cases. Using it might look something like this:
class Weird extends Error {
name = 'Weird'
}
class Fancy extends Error {
name = 'Fancy'
}
let weird = new Weird("something went wrong deep in the stack!");
let fancy = new Fancy("we handle it later", { cause: weird });
let topLevel = new Error("and handle *that* later too", { cause: fancy });
You can chain as many Error classes with a cause as you like, and this can provide extremely useful context for debugging.
Actually using that error cause effectively requires a bit of work. Most browsers print any attached causes in a reasonably nice cascade with console.error or similar. However, the string representation including the cause like this is not part of the specification for cause, so not everything supports it. Safari does not print the cause stack by default, for one.1 For another, existing logging infrastructure (especially if written by hand!) may not use it either.
Here’s a zero-dependencies2 version of little TypeScript utility I wrote while working on a new feature for True Myth yesterday:3
function printError(error: Error): string {
let maybeCause =
error.cause instanceof Error ? printError(error.cause) :
error.cause ? error.cause.toString() :
null;
let cause = maybeCause ? `\n\tcaused by: ${maybeCause}` : '';
return `${error.name}: ${error.message}${cause}`;
}
Given the topLevel error shown above, you can now log it with this formatting:
console.log(printError(topLevel));
The output will look like this:
Error: and handle *that* later too
caused by: Fancy: we handle it later
caused by: Weird: something went wrong deep in the stack!
If you wanted to, you could make this, or something like it, the body of an Error subclass you enforce using as your library or application’s error type:4
class MyAppError extends Error {
get name() {
return 'MyAppError';
}
toString() {
return printError(this);
}
}
Now calling toString() on any instance of MyAppError or any of its subclasses will produce the same nicely printed stack of errors! If you’d like to play around with this further, check out this TypeScript playground.
One final note: a cause can be anything. It does not have to be another Error. This is why the printError calls toString() on non-Error items. Note, however, that you will only get the level of nice formatting out of those objects that they support with their own toString implementations. A plain-old JS object will show up as the not-so-helpful message caused by: [object Object]. This is just JS being JS, though!
Notes
Dear Safari team, if you read this, please add support for this. It’s a small thing but fixing it would make workign with Safari as a primary dev browser a much nicer experience! ↩︎
The version I actually used in True Myth uses our
Maybetype because why wouldn’t it? ↩︎I know, I said
Taskwas feature-complete. But then someone asked about retries and off I went. I’ll probably have v8.5.0 out later today with a really nice API for retrying tasks! ↩︎I use a getter here for
nameto avoid creating an additional instance property on every singleMyAppError. ↩︎