Isomorphic Error Handling

Isomorphic Error Handling

Sep 09, 2025

image

If you’ve been writing JavaScript for a while, you’ve probably found yourself drowning in nested try...catch blocks. We've all been there—writing functions that throw errors, wrapping them in try-catch statements, and then realizing we need to handle even more potential failures. Before you know it, your code looks like a Russian nesting doll of error handling.

What if I told you there’s a better way? Enter isomorphic error handling — a pattern that can transform how you deal with exceptions in JavaScript.

The Problem with Traditional Error Handling

Let’s face it: try...catch blocks can be messy. They capture variables in their scope, forcing you to declare variables outside the block. Deep nesting makes your code harder to read and maintain. And when you're dealing with multiple functions that might throw errors, things get complicated fast.

Here’s what we’re trying to solve:

  • Reducing code duplication

  • Improving maintainability

  • Creating predictable behavior when failures occur

  • Standardizing error formats across your application

A Simple Solution: The Error Wrapper

Let me show you a pattern that’s been gaining traction in the JavaScript community. Instead of scattering try-catch blocks throughout your code, we can create a simple wrapper function:

// Error handling wrapper
function tryCatch(fn) {
    try {
        return [fn(), null];
    } catch(err) {
        return [null, err];
    }
}

This little function takes another function as a parameter and returns an array with two elements: [result, error]. If the function executes successfully, you get [result, null]. If it fails, you get [null, error].

Let’s see it in action:

// A function that might throw an error
const sqrt = (x) => {
    if (x < 0) throw new Error(`Number ${x} is invalid`);
    return Math.sqrt(x);
}
// Example with an error
const [res1, err1] = tryCatch(() => sqrt(-4));
if (err1) console.error(err1);
else console.log("sqrt(-4):", res1);// Example with a successful result
const [res2, err2] = tryCatch(() => sqrt(4));
if (err2) console.error(err2);
else console.log("sqrt(4):", res2);

The output would be:

Error: Number -4 is invalid
sqrt(4): 2

Working with Classes and Constructors

This pattern works beautifully with class constructors too. Let’s say you have a Person class that validates input:

class Person {
    constructor(name, age) {
        if (age < 0) throw `Invalid age ${age}. Minimum value: 1`;
        if (name.length < 2) throw `Invalid name ${name}: minimum length is 2 characters`;
        this.name = name;
        this.age = age;
    }
    
    print() {
        console.log(`Name: ${this.name} Age: ${this.age}`);
    }
}
// Using our wrapper with constructors
const [tom, err1] = tryCatch(() => new Person("Tom", -123));
if (err1) console.error(err1);
else tom.print();const [bob, err2] = tryCatch(() => new Person("Bob", 46));
if (err2) console.error(err2);
else bob.print();

Output:

Invalid age -123. Minimum value: 1
Name: Bob Age: 46

Making It Even Better

One issue you might notice is that not all errors in JavaScript are Error objects. Sometimes developers throw strings, numbers, or other types. We can enhance our wrapper to handle this:

function tryCatch(fn) {
    try {
        return [fn(), null];
    } catch(err) {
        // Ensure the error is an Error object
        const error = err instanceof Error ? err : new Error(String(err));
        return [null, error];
    }
}

Alternative Approach: The Curry Pattern

If you want to get fancy, you can create a curried version that’s even more flexible:

const tryWrap = (fn) => (...args) => {
    try {
        return [fn(...args), null];
    } catch(err) {
        return [null, err];
    }
}
// Usage examples
const [res1, err1] = tryWrap(sqrt)(-4);
const [res2, err2] = tryWrap(sqrt)(4);// With constructors
const [tom, tomErr] = tryWrap((name, age) => new Person(name, age))("Tom", -123);
const [bob, bobErr] = tryWrap((name, age) => new Person(name, age))("Bob", 46);

Building Functions That Support the Pattern

You can also design your functions from the ground up to support this pattern, eliminating the need for wrappers:

// Function that returns data in "[result, error]" format
const sqrt = (x) => {
    if (x < 0) return [null, new Error(`Number ${x} is invalid`)];
    return [Math.sqrt(x), null];
}
const [res1, err1] = sqrt(-4);
if (err1) console.error(err1);
else console.log("sqrt(-4):", res1);const [res2, err2] = sqrt(4);
if (err2) console.error(err2);
else console.log("sqrt(4):", res2);

Why This Pattern Rocks

This approach brings several benefits to your codebase:

Cleaner Code: No more nested try-catch blocks cluttering your logic.

Functional Style: This pattern plays beautifully with functional programming approaches.

Consistency: All your functions return results in the same format.

Easier Testing: You can test both success and error cases more predictably.

Better Monitoring: Centralized error handling makes it easier to log and monitor issues.

Async-Friendly: This pattern can be easily adapted for async/await operations.

The Bottom Line

Isomorphic error handling isn’t just a fancy term — it’s a practical approach that can make your JavaScript code more maintainable and predictable. By standardizing how you handle errors across your application, you reduce complexity and make your code easier to reason about.

The next time you find yourself writing yet another try-catch block, consider whether this pattern might serve you better. Your future self (and your teammates) will thank you for the cleaner, more consistent code.

Remember: good error handling isn’t about avoiding errors — it’s about handling them gracefully when they inevitably occur. This pattern gives you the tools to do exactly that.

Enjoy this post?

Buy DelphiFan Forum a coffee

More from DelphiFan Forum