Advanced25 min read

Error Handling

Learn to handle runtime errors gracefully with try/catch/finally, throw custom errors, and understand error types.

Why Error Handling?

Runtime errors are a fact of life in programming. Without proper handling, a single error can crash your entire program and leave users staring at a broken page.

Not all errors are bugs in your code. Many come from external sources you cannot control:

  • Network failures — API calls that timeout or return errors
  • Bad user input — unexpected formats, missing fields, invalid data
  • Missing data — accessing properties on null or undefined
  • Parse errors — invalid JSON from a server response
  • Browser differences — APIs not available in all browsers

Error handling allows your program to detect these problems and respond gracefully instead of crashing:

javascript
// Without error handling — crashes!
const data = JSON.parse('invalid json'); // SyntaxError!
console.log('This never runs');

// With error handling — recovers gracefully
try {
  const data = JSON.parse('invalid json');
} catch (error) {
  console.log('Invalid data, using defaults');
  const data = {};
}
console.log('Program continues running');

Good error handling improves user experience (showing helpful messages instead of blank screens), debugging (logging meaningful error info), and reliability (keeping the app running when things go wrong).

try / catch / finally

The try...catch statement lets you attempt risky code and handle any errors that occur:

javascript
try {
  // Code that might throw an error
  const result = riskyOperation();
  console.log(result);
} catch (error) {
  // Code that runs if an error is thrown
  console.log('Something went wrong:', error.message);
}

The error object in the catch block has useful properties:

  • error.message — a human-readable description of the error
  • error.name — the error type (e.g., 'TypeError', 'SyntaxError')
  • error.stack — a stack trace showing where the error occurred (useful for debugging)

The finally block runs no matter what — whether the code succeeded or failed. It is perfect for cleanup operations:

javascript
let connection;
try {
  connection = openDatabase();
  const data = connection.query('SELECT * FROM users');
  processData(data);
} catch (error) {
  console.log('Database error:', error.message);
} finally {
  // Always close the connection, even if there was an error
  if (connection) {
    connection.close();
  }
}

You can also nest try...catch blocks for more granular error handling:

javascript
try {
  try {
    JSON.parse(input);
  } catch (parseError) {
    console.log('Parse failed, trying alternative...');
    JSON.parse(alternativeInput);
  }
} catch (error) {
  console.log('All parsing failed:', error.message);
}

Throwing Errors

You can throw your own errors using the throw statement. This is useful for enforcing validation rules and function contracts:

javascript
function divide(a, b) {
  if (b === 0) {
    throw new Error('Cannot divide by zero');
  }
  return a / b;
}

try {
  const result = divide(10, 0);
} catch (error) {
  console.log(error.message); // 'Cannot divide by zero'
}

JavaScript has several built-in error types, each signaling a different kind of problem:

  • TypeError — wrong type: null.property, calling a non-function
  • ReferenceError — using an undefined variable
  • RangeError — value out of range: new Array(-1)
  • SyntaxError — invalid code: JSON.parse('{')
  • URIError — malformed URI: decodeURI('%')

You can throw specific error types for clarity:

javascript
function setAge(age) {
  if (typeof age !== 'number') {
    throw new TypeError('Age must be a number');
  }
  if (age < 0 || age > 150) {
    throw new RangeError('Age must be between 0 and 150');
  }
  return age;
}

You can also create custom error classes for your application:

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

throw new ValidationError('email', 'Invalid email format');

Error Handling Patterns

Here are practical patterns for handling errors effectively:

1. Validate input early (guard clauses):

javascript
function processUser(user) {
  if (!user) throw new Error('User is required');
  if (!user.email) throw new Error('Email is required');
  // ... proceed with valid data
}

2. Wrap risky operations in try/catch:

javascript
function loadConfig(jsonString) {
  try {
    return JSON.parse(jsonString);
  } catch (error) {
    console.warn('Invalid config, using defaults');
    return { theme: 'dark', language: 'en' };
  }
}

3. Do not catch errors you cannot handle — let them bubble up to a higher-level handler:

javascript
function readFile(path) {
  // If this fails, let the caller handle it
  // Don't catch and silently swallow the error
  return fs.readFileSync(path, 'utf8');
}

4. Re-throw errors when you need to log but not fully handle them:

javascript
try {
  processPayment(order);
} catch (error) {
  console.error('Payment failed:', error);
  throw error; // re-throw for the caller to handle
}

5. Error boundaries — in frameworks like React, you can catch rendering errors at a component level so one broken component does not crash the entire app. The concept applies broadly: catch errors at appropriate boundaries in your application.

Error Handling in Action

html
<div id="output"></div>

<script>
  function safeParseJSON(str) {
    try {
      const data = JSON.parse(str);
      return { success: true, data };
    } catch (error) {
      return { success: false, error: error.message };
    }
  }

  // Test with valid JSON
  const valid = safeParseJSON('{"name": "Alice", "age": 30}');

  // Test with invalid JSON
  const invalid = safeParseJSON('not valid json');

  const output = document.getElementById('output');
  output.innerHTML = `
    <p><strong>Valid JSON:</strong> ${valid.success ? valid.data.name : 'Failed'}</p>
    <p><strong>Invalid JSON:</strong> ${invalid.success ? 'Parsed' : invalid.error}</p>
  `;
</script>

When does the finally block execute?

Ready to practice?

Create your free account to access the interactive code editor, run challenges, and track your progress.