The Setup
So, I was recently using Node's url
module within TypeScript to be able to do some simple validation of user-provided URLs. According to the docs, when an invalid URL is supplied to the URL
class, it throws a TypeError
. Great! This is exactly what I wanted.
Next, all I had to do was catch that particular TypeError
and give a helpful message to the user to let them know their URL was no good. Easy, all I need to do is write a try-catch
statement and check the error's code
. Of course, the specific error code to look for is documented on an entirely different page for some reason. It was actually easier for me to just spin up a terminal and write a gibberish string into a new URL()
call myself to determine that I was looking for "ERR_INVALID_URL"
.
The Problematic Code
try {
const validUrl = new URL(url).href;
} catch (e) {
if (e instanceof TypeError) {
if (e.code === "ERR_INVALID_URL") {
// Property 'code' does not exist on
// type 'TypeError'. ts(2339)
}
}
}
Huh? What do you mean? The docs clearly stated that an Error
in Node should have a code
property, and TypeError
extends Error
... This didn't make sense.
I used VS Code's nifty "Go to Definition" feature to find the type definition for TypeError
, which opened node_modules\typescript\lib\lib.es5.d.ts
. I then found my way to the definition for the Error
interface...
/* node_modules\typescript\lib\lib.es5.d.ts */
interface Error {
name: string;
message: string;
stack?: string;
}
Oh! This was the interface of an Error you'd find in a browser environment.
But I was working with Node, and I already had the @types/node package installed... I had falsely assumed that this would somehow magically tell the TypeScript linter that I was catching a Node Error
. How was I supposed to get TypeScript to infer that the TypeError
I was handling most likely extended Node's Error
class, and had the extra code
property I was looking for?
Search Engine Spelunking
After some confused finagling with my tsconfig.json
and VS Code settings, I quickly gave up and went to Google. I soon after learned two things via random answers on StackOverflow:
-
The type definition for the NodeJS
Error
class is declared innode_modules\@types\node\globals.d.ts
-- and was accessible asNodeJS.ErrnoException
. I wasn't sure where this was officially documented, but alright!
/* node_modules\@types\node\globals.d.ts */ interface ErrnoException extends Error { errno?: number; code?: string; path?: string; syscall?: string; stack?: string; }
It was possible to use TypeScript's type guards to create a function that I could use to check the error at runtime, so that I (and TypeScript) could be absolutely sure that this variable was a Node
Error
.
The example function from StackOverflow looked sort of like this:
function isError(error: any): error is NodeJS.ErrnoException {
return error instanceof Error;
}
At a glance, this seemed like it would work... The function was running an instanceof
check and used a "type predicate" (the error is NodeJS.ErrnoException
part) to help TypeScript to do the type inference I was looking for. I could finally access the code
property on the error without any dreaded red squiggly lines.
if (isError(e) && e instanceof TypeError) {
if (e.code === "ERR_INVALID_URL") {
// Hooray?
}
}
But, I wasn't totally satisfied. For one, there was nothing stopping me from passing things that weren't errors to isError()
. This was easily fixed by changing the the first argument of isError()
to expect Error
instead of any
.
Secondly, it also felt inherently silly to have to run two instanceof
checks every time I wanted to handle an error. (Truthfully, it's not the worst thing in the world... but I believe that TypeScript should require developers to make as few runtime code changes as possible when transitioning from JavaScript.)
The Solution
After some experimenting, I managed to come up with the following function, which I tested with a couple of custom error classes to ensure that any additionally defined properties were preserved.
It turned out that the key was to make a generic function which acted as a typeguarded version of instanceof
for Node.JS error handling, by doing the following things:
Accepted two arguments that would be similar to the left-hand and right-hand sides of the
instanceof
operator.Enforced the first argument was of the
Error
class or a subclass.Enforced the second argument was a constructor for an
Error
or a subclass ofError
.Ran the
instanceof
check.Used a type predicate to intersect the type of the first argument with the instance type of the error constructor in the second argument, as well as
NodeJS.ErrnoException
so that type inference would work as expected when used.
/**
* A typeguarded version of `instanceof Error` for NodeJS.
* @author Joseph JDBar Barron
* @link https://dev.to/jdbar
*/
export function instanceOfNodeError<T extends new (...args: any) => Error>(
value: Error,
errorType: T
): value is InstanceType<T> & NodeJS.ErrnoException {
return value instanceof errorType;
}
Examples
Original Use Case
try {
const validUrl = new URL(url).href;
} catch (e) {
if (instanceOfNodeError(e, TypeError)) {
if (e.code === "ERR_INVALID_URL") {
// Hooray!
}
}
}
Usage with Custom Error Classes
// Create our custom error classes.
class CoolError extends Error {
foo: string = "Hello world.";
}
class VeryCoolError extends CoolError {
bar: string = "Goodbye world.";
}
// Try throwing an error.
try {
throw new CoolError();
} catch (e) {
if (instanceOfNodeError(e, CoolError)) {
// typeof e: CoolError & NodeJS.ErrnoException
console.log(e.foo);
} else if (instanceOfNodeError(e, VeryCoolError)) {
// typeof e: VeryCoolError & NodeJS.ErrnoException
console.log(e.foo, e.bar);
} else {
// typeof e: any
console.log(e);
}
}
// Try passing something that's not an error.
const c = NaN;
if (instanceOfNodeError(c, CoolError)) {
// Argument of type 'number' is not assignable to\
// parameter of type 'Error'. ts(2345)
console.log(c.foo);
}
const d = new CoolError();
if (instanceOfNodeError(d, Number)) {
// Argument of type 'NumberConstructor' is not assignable
// to parameter of type 'new (...args: any) => Error'.
console.log(d.foo);
}
You might be wondering why in that one else
clause, the type of e
was any
... well, TypeScript can't guarantee the type of e
is anything in particular, because JavaScript lets you throw
literally anything. Thanks JavaScript...
Summary
After utilizing both generics and type guards, I managed to get TypeScript to correctly infer the shape of the errors I was handling in a Node.js environment without performing redundant instanceof
checks. However, the solution still wasn't perfect, since I probably did sacrifice some amount of compute overhead and space on the call stack to be able to call the instanceOfNodeError()
function compared to the bog-standard instanceof
call I would have done in JavaScript.
It's possible that in the future, there could be an update to the @types/node package that would merge the NodeJS.ErrnoException
type with the global Error
type.
One could argue that since not all errors in Node.js will have the code
property (or the other properties on the ErrnoException
type), that it doesn't make sense to do such a reckless merging of types. However, I don't see a lot of harm when all of the properties of ErrnoException
are marked optional.
Otherwise, they have to be manually added to any modules that might throw an Error
with the properties of ErrnoException
, per the details of this rather old commit responsible for implementing it within the fs
module. However, this still leaves us with a problem when these ErrnoException
errors can be thrown by the constructors of classes in Node.js, like the URL
class does.
For that, the only alternative fix I could think of would be for TypeScript to add some sort of throws
syntax for function/constructor signatures -- which there seems to be an open issue for from 2016 in the microsoft/TypeScript GitHub repo.
Top comments (0)