Skip to content
JavaScript Error Handling Explained — Try/Catch, Error Types & Global Handling

JavaScript Error Handling Explained — Try/Catch, Error Types & Global Handling

DodaTech Updated Jun 15, 2026 9 min read

JavaScript error handling is the practice of catching, managing, and recovering from runtime errors so your application doesn’t crash unexpectedly. Every real-world app — from the Doda Browser to Durga Antivirus Pro — relies on structured error handling to stay stable when things go wrong.

What You’ll Learn

  • The try/catch/finally pattern and how it works
  • Built-in Error types — SyntaxError, ReferenceError, TypeError, RangeError
  • Creating custom error classes with proper stack traces
  • Global error handlers — window.onerror and window.onunhandledrejection
  • Error handling patterns for async code (callbacks, promises, async/await)

Why Error Handling Matters

Without error handling, a single failed API call or a null reference can crash your entire app. In the Durga Antivirus Pro dashboard, one unhandled error during a threat scan could freeze the UI and prevent the user from seeing critical alerts. Proper error handling keeps your app running gracefully, logs what went wrong, and often lets the user continue without even noticing there was a problem.

Learning Path

    flowchart LR
  A[Async JavaScript] --> B[Error Handling]
  B --> C[Fetch API & HTTP]
  B --> D[Closures & Scope]
  B --> E[You Are Here]
  
Prerequisites: You should understand JavaScript functions and be comfortable with JavaScript basics. Familiarity with Async Await is helpful but not required.

The try/catch/finally Pattern

The core of JavaScript error handling is the try/catch/finally block. Think of it like a safety net: you try something risky, catch the error if it fails, and run cleanup code either way.

try {
  // Code that might throw
  const data = JSON.parse('{"invalid": broken}');
  console.log('Parsed:', data);
} catch (error) {
  // Runs only if try block throws
  console.error('Parse failed:', error.message);
  // Output: Parse failed: Unexpected token b in JSON at position 14
} finally {
  // Always runs — even if catch re-throws
  console.log('Cleanup complete');
  // Output: Cleanup complete
}

Line by line:

  • try — Wrap code that might throw an error. If everything goes well, catch is skipped.
  • catch(error) — Catches any error thrown in the try block. The error object has name, message, and stack properties.
  • finally — Always runs, whether there was an error or not. Use it for closing files, hiding spinners, or cleaning up.

Anticipating confusion: You don’t need catch and finally together. You can use try/finally (when you want cleanup but don’t care about the error) or try/catch (when you want to handle errors but don’t need cleanup).

Built-in Error Types

JavaScript provides several built-in error constructors, each for a specific kind of failure:

// ReferenceError — accessing undefined variable
console.log(nonExistent);
// Uncaught ReferenceError: nonExistent is not defined

// TypeError — calling something that's not a function
const num = 42;
num(); // Uncaught TypeError: num is not a function

// SyntaxError — invalid language syntax
eval('hello world');
// Uncaught SyntaxError: Unexpected identifier

// RangeError — value out of allowed range
new Array(-1);
// Uncaught RangeError: Invalid array length

When each type fires:

  • ReferenceError — You tried to use a variable that doesn’t exist or is in the temporal dead zone.
  • TypeError — You performed an operation on a value of the wrong type (like calling a number).
  • SyntaxError — The JavaScript parser found invalid syntax. Only happens in eval() or parsing code at runtime.
  • RangeError — A numeric value is outside its allowed range.
  • URIErrordecodeURI() or encodeURI() received malformed input.

Custom Error Classes

For real applications, you want specific error types — not just generic Error objects. Extend the Error class to create your own:

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

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

try {
  throw new ValidationError('Email is required', 'email');
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(`Field: ${error.field}`);  // 'email'
    console.log(error.message);             // 'Email is required'
  }
}

Why custom errors matter: In a large app like Doda Browser, different parts of the code need to handle errors differently. A NetworkError might trigger a retry, while a ValidationError should show a message to the user. Custom error classes let you distinguish between these cases with instanceof.

The throw Statement

You can throw any value — but always throw an Error object for proper stack traces:

function divide(a, b) {
  if (b === 0) {
    throw new Error('Division by zero');
  }
  if (typeof a !== 'number' || typeof b !== 'number') {
    throw new TypeError('Both arguments must be numbers');
  }
  return a / b;
}

try {
  divide(10, 0);
} catch (err) {
  console.log(err.message);   // 'Division by zero'
  console.log(err.stack);     // Full stack trace
}

Rule of thumb: Always throw new Error(...), never throw "string". Error objects give you stack traces that tell you exactly where the error happened.

Global Error Handling

Errors that aren’t caught anywhere bubble up to the global scope. Use these handlers as a last resort:

// Catch all uncaught errors
window.onerror = function(message, source, line, column, error) {
  console.error('Global error:', message);
  // Log to your error reporting service
  return true; // Prevents the default browser error handling
};

// Catch unhandled promise rejections
window.onunhandledrejection = function(event) {
  console.error('Unhandled rejection:', event.reason);
  event.preventDefault();
};

In Node.js, the equivalents are process.on('uncaughtException') and process.on('unhandledRejection').

Async Error Handling

Async code needs special care. A thrown error inside a setTimeout or promise chain won’t be caught by an outer try/catch:

// ❌ This won't catch the error
try {
  setTimeout(() => {
    throw new Error('Async error');
  }, 1000);
} catch (err) {
  console.log('Caught?');  // Never runs
}

// ✅ Catch inside the async callback
setTimeout(() => {
  try {
    throw new Error('Async error');
  } catch (err) {
    console.log('Caught!');  // Runs
  }
}, 1000);

Fetch with Error Handling

A complete, real-world pattern for handling fetch errors:

async function fetchUserData(userId) {
  try {
    const response = await fetch(`https://api.example.com/users/${userId}`);

    if (!response.ok) {
      throw new NetworkError(
        `Request failed with status ${response.status}`,
        response.status
      );
    }

    const data = await response.json();

    if (!data.id) {
      throw new ValidationError('Invalid user data received', 'userId');
    }

    return data;

  } catch (error) {
    if (error instanceof NetworkError) {
      // Retry logic or show offline message
      console.warn('Network issue, will retry...');
      throw error; // Re-throw if caller needs to know
    }
    if (error instanceof ValidationError) {
      console.error('Bad data from server:', error.message);
      return null; // Graceful degradation
    }
    // Unknown error — log and re-throw
    console.error('Unexpected error:', error);
    throw error;
  }
}

Why this pattern works: Each error type gets its own handling strategy. Network errors trigger retries. Validation errors return null gracefully. Unknown errors bubble up to the global handler.

Common Mistakes

1. Swallowing errors silently

try {
  riskyOperation();
} catch (err) {
  // Do nothing — error is lost forever!
}

Fix: At minimum log the error. Never leave empty catch blocks.

2. Catching without re-throwing when you can’t handle it

try {
  // Some operation
} catch (err) {
  console.log('Error happened'); // But you can't actually recover
}

Fix: Re-throw errors you can’t handle: throw err;

3. Not distinguishing error types

catch (err) {
  // Same handling for ReferenceError and TypeError
}

Fix: Use instanceof checks to handle different errors differently.

4. Throwing non-Error values

throw 'Something went wrong'; // No stack trace!

Fix: Always throw new Error('message').

5. Forgetting try/catch in async functions

async function load() {
  const data = await fetch('/api/data'); // If this fails, it's unhandled
}

Fix: Wrap in try/catch or add .catch() to the returned promise.

Practice Questions

  1. What’s the difference between throw new Error() and throw "string"? new Error() creates an object with name, message, and stack properties. Throwing a string gives you no stack trace, making debugging much harder.

  2. Does finally always run? Yes — even if the catch block re-throws, or the try block has a return statement. finally runs before control leaves the try/catch/finally structure.

  3. Why can’t an outer try/catch catch an error inside setTimeout? The setTimeout callback runs later, after the try/catch block has already finished. The error is thrown in a new execution context.

  4. What is window.onerror used for? It’s a global error handler that catches any uncaught error in the window. Use it as a last resort for logging errors to your monitoring service.

Challenge: Create a retry(fn, maxAttempts, delay) function that retries an async operation up to maxAttempts times with a delay between attempts. Use custom error types to distinguish between retryable and non-retryable errors.

Mini Project: Safe JSON Parser

Build a function that parses JSON safely and reports structured errors:

class ParseResult {
  constructor(data, error) {
    this.data = data;
    this.error = error;
  }
  get success() { return this.error === null; }
}

function safeParse(jsonString) {
  try {
    const data = JSON.parse(jsonString);
    return new ParseResult(data, null);
  } catch (error) {
    if (error instanceof SyntaxError) {
      return new ParseResult(null, {
        type: 'invalid_json',
        message: error.message,
        position: error.message.match(/position (\d+)/)?.[1]
      });
    }
    return new ParseResult(null, {
      type: 'unknown',
      message: error.message
    });
  }
}

const result = safeParse('{"name": "Alice"}');
console.log(result.success); // true
console.log(result.data);    // { name: 'Alice' }

FAQ

What is the difference between throw and return?
return exits a function normally with a value. throw exits abnormally with an error, and execution jumps to the nearest catch block.
Should I catch every error?
No. Only catch errors you can meaningfully handle. If you can’t recover, let the error propagate to a global handler or the caller.
What is the temporal dead zone?
The TDZ is the period between entering a scope and the variable’s declaration where let and const variables exist but cannot be accessed.
How do I get a stack trace?
All Error objects have a .stack property that shows the call stack at the point where the error was thrown.
What happens if finally throws?
If finally throws, any pending exception from try or catch is replaced by the new exception.

What’s Next

LessonDescription
JavaScript HomeBack to the JavaScript hub
https://tutorials.dodatech.com/programming-languages/javascript/js-async/Async JavaScript — promises, async/await
https://tutorials.dodatech.com/programming-languages/javascript/js-modules/Modules & dynamic imports
https://tutorials.dodatech.com/programming-languages/javascript/js-fetch-api/Fetch API — HTTP requests
Node.jsError handling in Node.js

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

What’s Next

Congratulations on completing this JavaScript Error Handling tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro