Right, let’s talk about one of the few things that can make error handling in TypeScript feel almost civilized: the instanceof pattern. You see, JavaScript’s native Error type is tragically anemic. It’s a blank slate with a message property and, if you’re lucky, a usable stack trace. Throwing around generic Error objects is like trying to fix a precision watch with a sledgehammer—it gets the job done, but good luck figuring out what went wrong afterwards.

The core idea here is beautifully simple: instead of throwing a generic Error, you throw an instance of a specific class that extends Error. Then, in your catch block, you can use the instanceof operator to check exactly what kind of error you’ve caught. This moves you from vague panic to targeted handling.

Why instanceof Works (And typeof Doesn’t)

First, a crucial technicality. You might think, “I’ll just use typeof on the error’s name property.” Please, don’t. That’s a path to madness. The instanceof operator checks the prototype chain of an object. When you write err instanceof DatabaseConnectionError, the JavaScript runtime is checking if DatabaseConnectionError’s prototype is anywhere in the chain of err’s prototypes.

This is robust. It works across execution contexts (like iframes or different Node.js modules), which comparing constructor names or typeof can fail at. It’s the semantic way to check an object’s class.

Here’s the absolute, non-negotiable baseline for creating a typed error class. Pay attention to the Object.setPrototypeOf call—this is the secret sauce.

class DatabaseConnectionError extends Error {
    public readonly url: string;
    public readonly port: number;

    constructor(message: string, url: string, port: number) {
        super(message);
        this.url = url;
        this.port = port;
        this.name = 'DatabaseConnectionError'; // Makes stack traces clearer

        // This is critical! Restores the correct prototype chain
        // that was broken by extending a built-in class.
        Object.setPrototypeOf(this, DatabaseConnectionError.prototype);
    }
}

// Somewhere deep in your database driver code...
throw new DatabaseConnectionError('Connection timed out', 'db.example.com', 5432);

Without that Object.setPrototypeOf line (or using a modern target like ES2022 which handles it for you), err instanceof DatabaseConnectionError would return false in your catch block. It’s a bizarre JavaScript quirk, and forgetting it is the number one rookie mistake with this pattern.

The Catch Block Becomes Actually Useful

Now, look at what your catch block can become. Instead of a desperate scramble to inspect string messages, you have a clear, type-safe decision tree.

try {
    await connectToDatabase();
} catch (err: unknown) {
    // First, always ensure it's *an* Error. This is TypeScript 101.
    if (!(err instanceof Error)) {
        throw err; // Re-throw the weird non-error thing, we can't handle it.
    }

    // Now, we can discriminate based on type.
    if (err instanceof DatabaseConnectionError) {
        // TypeScript now knows `err` is a DatabaseConnectionError!
        console.error(`Failed to connect to ${err.url}:${err.port}`);
        await startFallbackService();
    } else if (err instanceof AuthenticationError) {
        console.error('Bad credentials!');
        await promptForNewPassword();
    } else {
        // It's some other Error we didn't explicitly expect.
        throw err; // Re-throw it; let someone else handle it.
    }
}

See how clean that is? The type narrowing within each if block is perfect. You’re not just handling an error; you’re handling a known, structured event with actionable data.

The Inevitable “But What About async/await?”

Ah, yes. Here’s the rough edge the designers gifted us. The instanceof check relies on the prototype chain. If your try block is in one file (or module) and the error is thrown from a completely different, separately compiled package, you might run into issues where two identical DatabaseConnectionError classes have two different prototype objects.

It’s rare, but it happens. The most robust workaround is to add a unique, branded property to your error classes—a kind or code string literal that you can check instead. It’s less elegant than instanceof but guaranteed to work across module boundaries.

class NetworkTimeoutError extends Error {
    // The branded property
    readonly kind = 'NetworkTimeoutError' as const;
    readonly durationMs: number;

    constructor(durationMs: number) {
        super(`Network request timed out after ${durationMs}ms`);
        this.durationMs = durationMs;
        Object.setPrototypeOf(this, NetworkTimeoutError.prototype);
    }
}

// In your catch block:
if (err instanceof Error && 'kind' in err) {
    if (err.kind === 'NetworkTimeoutError') {
        // You can now safely cast it because you checked the brand.
        const timeoutErr = err as NetworkTimeoutError;
        console.log(`Timeout was: ${timeoutErr.durationMs}`);
    }
}

It’s a concession to practicality, but a smart one. You get most of the type safety with none of the cross-module fragility.

So, is the instanceof pattern the final word in error handling? No. But it’s a massive leap forward from the dark ages of string matching. It gives you structure, clarity, and type safety right where you need it most: when everything has already gone wrong. And frankly, anything that makes debugging easier is a win in my book.