DEV Community

Joseph Barron
Joseph Barron

Posted on

The Problem with Handling Node.js Errors in TypeScript (and the workaround)

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. The type definition for the NodeJS Error class is declared in node_modules\@types\node\globals.d.ts -- and was accessible as NodeJS.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;
    }
    
  2. 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;
}
Enter fullscreen mode Exit fullscreen mode

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?
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Accepted two arguments that would be similar to the left-hand and right-hand sides of the instanceof operator.

  2. Enforced the first argument was of the Error class or a subclass.

  3. Enforced the second argument was a constructor for an Error or a subclass of Error.

  4. Ran the instanceof check.

  5. 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;
}
Enter fullscreen mode Exit fullscreen mode

Examples

Original Use Case

try {
    const validUrl = new URL(url).href;
} catch (e) {
    if (instanceOfNodeError(e, TypeError)) {
        if (e.code === "ERR_INVALID_URL") {
            // Hooray!
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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)