Follow AiTechWorlds on LinkedIn for professional AI content!Follow Now →
14 minLesson 28 of 35
OOP & Advanced Concepts

Error Handling with try/catch

Error Handling with try/catch

Robust JavaScript handles errors gracefully — it doesn't crash the entire application when something unexpected happens. This lesson covers synchronous and asynchronous error handling, custom error types, and defensive programming patterns.

try/catch/finally

try {
    // Code that might throw
    const data = JSON.parse(malformedJson);
    processData(data);
} catch (error) {
    // Runs when an error is thrown
    console.error("Failed to parse:", error.message);
} finally {
    // ALWAYS runs — error or not
    closeConnection();
}

The Error Object

try {
    null.toString();  // TypeError
} catch (e) {
    e.name;       // "TypeError"
    e.message;    // "Cannot read properties of null"
    e.stack;      // full stack trace (string)
}

// Built-in error types
new Error("generic error");
new TypeError("wrong type");
new RangeError("value out of range");
new SyntaxError("bad syntax");
new ReferenceError("variable not defined");
new URIError("bad URL encoding");

Custom Error Classes

class AppError extends Error {
    constructor(message, statusCode = 500) {
        super(message);
        this.name = "AppError";
        this.statusCode = statusCode;
    }
}

class NotFoundError extends AppError {
    constructor(resource) {
        super(`${resource} not found`, 404);
        this.name = "NotFoundError";
        this.resource = resource;
    }
}

class ValidationError extends AppError {
    constructor(field, message) {
        super(`Validation failed: ${message}`, 400);
        this.name = "ValidationError";
        this.field = field;
    }
}

// Usage
function getUser(id) {
    const user = users.find(u => u.id === id);
    if (!user) throw new NotFoundError(`User #${id}`);
    return user;
}

// Handle different error types
try {
    const user = getUser(99);
} catch (err) {
    if (err instanceof NotFoundError) {
        showNotFound();
    } else if (err instanceof ValidationError) {
        showFieldError(err.field, err.message);
    } else {
        throw err;  // re-throw unexpected errors
    }
}

Async Error Handling

// async/await with try/catch
async function loadDashboard(userId) {
    try {
        const user = await fetchUser(userId);
        const posts = await fetchPosts(userId);
        return { user, posts };
    } catch (err) {
        if (err.status === 401) redirectToLogin();
        else if (err.status === 404) throw new NotFoundError(`User #${userId}`);
        else throw err;  // unexpected errors bubble up
    }
}

// Promise chains with .catch
fetchUser(userId)
    .then(user => fetchPosts(user.id))
    .then(posts => render(posts))
    .catch(err => handleError(err));

// Unhandled promise rejections — catch globally
window.addEventListener("unhandledrejection", (event) => {
    console.error("Unhandled promise rejection:", event.reason);
    event.preventDefault();  // prevent default browser behavior
    logToErrorService(event.reason);
});

Parallel Errors

// Promise.all — fails fast on any rejection
try {
    const [user, posts, settings] = await Promise.all([
        fetchUser(id),
        fetchPosts(id),
        fetchSettings(id)
    ]);
} catch (err) {
    // Any one failure cancels all (others' results are discarded)
    handleError(err);
}

// Promise.allSettled — handles mixed success/failure
const results = await Promise.allSettled([
    fetchUser(id),
    fetchPosts(id),
    fetchSettings(id)  // this one might fail
]);

results.forEach(result => {
    if (result.status === "fulfilled") {
        use(result.value);
    } else {
        console.error("Failed:", result.reason);
    }
});

Defensive Patterns

// Result type pattern — avoid exceptions for expected failures
function parseJSON(str) {
    try {
        return { ok: true, value: JSON.parse(str) };
    } catch {
        return { ok: false, error: "Invalid JSON" };
    }
}

const result = parseJSON(userInput);
if (result.ok) {
    process(result.value);
} else {
    showError(result.error);
}

// Retry with exponential backoff
async function fetchWithRetry(url, maxRetries = 3) {
    for (let attempt = 0; attempt < maxRetries; attempt++) {
        try {
            const response = await fetch(url);
            if (!response.ok) throw new Error(`HTTP ${response.status}`);
            return await response.json();
        } catch (err) {
            if (attempt === maxRetries - 1) throw err;  // last attempt
            const delay = 2 ** attempt * 1000;  // 1s, 2s, 4s
            await new Promise(resolve => setTimeout(resolve, delay));
        }
    }
}

Global Error Handlers

// Catch synchronous errors that escape try/catch
window.onerror = (message, source, line, col, error) => {
    reportError({ message, source, line, col, stack: error?.stack });
    return true;  // prevents default browser error display
};

// Catch unhandled promise rejections
window.addEventListener("unhandledrejection", (event) => {
    reportError(event.reason);
});

// In Node.js
process.on("uncaughtException", (err) => {
    console.error("Uncaught exception:", err);
    process.exit(1);  // crash with non-zero code
});

process.on("unhandledRejection", (reason, promise) => {
    console.error("Unhandled rejection:", reason);
});

When to Throw vs When to Return

// Throw for programmer errors (should never happen in correct code)
function getUser(id) {
    if (typeof id !== "number") {
        throw new TypeError(`Expected number, got ${typeof id}`);
    }
    // ...
}

// Return null/undefined for expected "not found" situations
function findUser(id) {
    return users.find(u => u.id === id) ?? null;
}

// Return a result object for user-facing validation
function validateEmail(email) {
    if (!email) return { valid: false, error: "Email is required" };
    if (!/@/.test(email)) return { valid: false, error: "Must contain @" };
    return { valid: true };
}

// Throw for unrecoverable failures that need to bubble up
async function getAuthenticatedUser() {
    const token = localStorage.getItem("token");
    if (!token) throw new AuthError("Not authenticated");  // calling code must handle
    
    const res = await fetch("/api/me", { headers: { Authorization: `Bearer ${token}` } });
    if (!res.ok) throw new AuthError("Token invalid");
    return res.json();
}

Next lesson: JavaScript Design Patterns — reusable solutions to common problems.

📱

Get this course's notes on Telegram!

Free cheat sheets, summaries & practice exercises

Get Notes Free →
!