8 Essential JavaScript Error‑Handling Techniques for More Resilient Apps

Master robust JavaScript error handling with eight practical strategies—including async try‑catch, global handlers, custom error types, graceful degradation, error boundaries, promise chain management, log level structuring, and automatic recovery—to boost application stability and improve user experience.

JavaScript
JavaScript
JavaScript
8 Essential JavaScript Error‑Handling Techniques for More Resilient Apps

Error handling is often overlooked, yet it determines an application's robustness and user experience. Here are eight practical JavaScript error‑handling techniques to help build more reliable applications.

1. Wrap asynchronous code with try‑catch

Many developers think try‑catch only works for synchronous code, but it can also gracefully handle asynchronous operations.

// ❌ Bad example
async function fetchUserData() {
    const response = await fetch('/api/users');
    const data = await response.json();
    return data;
}

// ✅ Good example
async function fetchUserData() {
    try {
        const response = await fetch('/api/users');
        const data = await response.json();
        return data;
    } catch (error) {
        // Distinguish error types
        if (error instanceof TypeError) {
            console.error('Network request failed:', error.message);
            // Optionally retry or return cached data
        } else {
            console.error('Data parsing failed:', error.message);
        }
        // Return default value or throw custom error
        return [];
    }
}

2. Implement a global error handler

A global error handler catches errors not captured by local try‑catch blocks, serving as the final defense for reliable applications.

// Handle regular JavaScript errors
window.onerror = function(message, source, lineno, colno, error) {
    console.error({message, source, lineno, colno, error: error?.stack});
    // Send report to monitoring service
    sendErrorReport({
        type: 'js_error',
        details: {message, source, lineno, colno}
    });
    return true; // Prevent further propagation
};

// Handle unhandled Promise rejections
window.addEventListener('unhandledrejection', function(event) {
    console.error('Unhandled Promise rejection:', event.reason);
    event.preventDefault(); // Prevent default handling
});

3. Create custom error types

Defining custom error classes helps differentiate and handle various error categories more effectively.

4. Graceful error degradation

When errors occur, provide sensible fallback solutions instead of letting the application crash.

5. Error boundary handling

In React, error boundaries prevent the whole app from crashing due to a local error; similar isolation can be implemented in plain JavaScript.

6. Optimize asynchronous error propagation chains

Properly handling error propagation in Promise chains is crucial.

7. Log level handling

Establishing a sensible log level system improves issue tracking and localization.

8. Error recovery mechanisms

Implement automatic recovery so the application can self‑heal after encountering errors.

class ServiceManager {
    constructor(services) {
        this.services = new Map(services);
        this.healthChecks = new Map();
        this.startMonitoring();
    }

    async startService(name) {
        try {
            const service = this.services.get(name);
            await service.start();
            this.monitorService(name);
        } catch (error) {
            console.error(`Service ${name} failed to start:`, error);
            this.attemptRecovery(name);
        }
    }

    monitorService(name) {
        const healthCheck = setInterval(async () => {
            try {
                const service = this.services.get(name);
                const isHealthy = await service.checkHealth();
                if (!isHealthy) {
                    throw new Error('Service health check failed');
                }
            } catch (error) {
                this.handleServiceFailure(name, error);
            }
        }, 30000);
        this.healthChecks.set(name, healthCheck);
    }

    async handleServiceFailure(name, error) {
        console.error(`Service ${name} error:`, error);
        clearInterval(this.healthChecks.get(name));
        this.healthChecks.delete(name);
        await this.attemptRecovery(name);
    }

    async attemptRecovery(name, retries = 3) {
        for (let i = 0; i < retries; i++) {
            try {
                console.log(`Attempting recovery for ${name}, attempt ${i + 1}`);
                await this.startService(name);
                console.log(`Service ${name} recovered`);
                return true;
            } catch (error) {
                console.error(`Recovery attempt ${i + 1} failed:`, error);
                await new Promise(resolve => setTimeout(resolve, 5000 * (i + 1)));
            }
        }
        console.error(`Service ${name} recovery failed after maximum retries`);
        return false;
    }
}

Good error handling goes beyond merely catching errors; it involves gracefully managing them to keep the application running, and it should be an ongoing process that evolves with the app.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaScriptbest practicesloggingError HandlingAsync
JavaScript
Written by

JavaScript

Provides JavaScript enthusiasts with tutorials and experience sharing on web front‑end technologies, including JavaScript, Node.js, Deno, Vue.js, React, Angular, HTML5, CSS3, and more.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.