Error Handling in JavaScript: try/catch and Custom Errors

Unhandled errors in JavaScript either crash a script or silently swallow a rejected Promise — proper error handling is what stands between a graceful failure and a broken page.

class ValidationError extends Error {
    constructor(message, field) {
        super(message);
        this.name = 'ValidationError';
        this.field = field;
    }
}

function validateAge(age) {
    if (age < 0) throw new ValidationError('Age cannot be negative', 'age');
    return age;
}

try {
    validateAge(-5);
} catch (err) {
    if (err instanceof ValidationError) {
        console.error(<code>Invalid ${err.field}: ${err.message}</code>);
    } else {
        throw err; // re-throw anything we don't specifically handle
    }
} finally {
    console.log('Validation attempt complete');
}

// Async errors need try/catch around await, OR a .catch() on the promise
async function loadUser(id) {
    try {
        const res = await fetch(<code>/api/users/${id}</code>);
        return await res.json();
    } catch (err) {
        console.error('Failed to load user:', err);
        return null;
    }
}

Custom error classes let calling code distinguish between error types with instanceof instead of parsing error message strings — genuinely important once an application has more than one or two kinds of failure to handle differently.