JavaScript Error Handling

Error handling allows your program to deal with unexpected situations gracefully instead of crashing. JavaScript provides try...catch...finally for handling errors, and the throw statement for creating custom errors.

try...catch

The try block contains code that might throw an error. The catch block runs only if an error occurs, receiving the error object as a parameter.

Basic try...catch
// Catching an error
try {
  const data = JSON.parse('invalid json');
} catch (error) {
  console.log(error.name);    // 'SyntaxError'
  console.log(error.message); // 'Unexpected token...'
}
console.log('Code continues after catch');

// Without try...catch, the error would crash the program
// With try...catch, we handle it gracefully

// catch binding is optional (ES2019+)
try {
  JSON.parse('{}');
  console.log('Valid JSON parsed successfully');
} catch {
  console.log('Failed to parse');
}

The finally Block

The finally block executes regardless of whether an error occurred or not. It is commonly used for cleanup operations like closing connections or releasing resources.

try...catch...finally
function readData() {
  console.log('Opening resource...');
  try {
    console.log('Reading data...');
    throw new Error('Read failed');
  } catch (error) {
    console.log('Error: ' + error.message);
  } finally {
    console.log('Closing resource (always runs)');
  }
}
readData();
// Opening resource...
// Reading data...
// Error: Read failed
// Closing resource (always runs)

// finally runs even with return statements
function test() {
  try {
    return 'try';
  } finally {
    console.log('finally runs before return');
  }
}
console.log(test());
// finally runs before return
// 'try'
📝 Note: The finally block always executes, even if the try or catch block contains a return statement. This makes it ideal for cleanup code.

The throw Statement

The throw statement lets you create custom errors. You can throw any value, but it is best practice to throw Error objects so they include a stack trace.

Throwing Errors
// Throw a built-in Error
function divide(a, b) {
  if (b === 0) {
    throw new Error('Division by zero');
  }
  return a / b;
}

try {
  console.log(divide(10, 2));  // 5
  console.log(divide(10, 0));  // throws!
} catch (e) {
  console.log(e.message); // 'Division by zero'
}

// Throw specific error types
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');
  }
  console.log('Age set to ' + age);
}

try {
  setAge('old');
} catch (e) {
  console.log(e.name + ': ' + e.message);
  // TypeError: Age must be a number
}

Custom Error Classes

You can create custom error classes by extending the built-in Error class. This lets you create application-specific error types that can be caught selectively.

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

class NotFoundError extends Error {
  constructor(resource) {
    super(`${resource} not found`);
    this.name = 'NotFoundError';
    this.resource = resource;
  }
}

function validateUser(user) {
  if (!user.name) {
    throw new ValidationError('name', 'Name is required');
  }
  if (!user.email) {
    throw new ValidationError('email', 'Email is required');
  }
}

try {
  validateUser({ name: 'Alice' });
} catch (e) {
  if (e instanceof ValidationError) {
    console.log(`${e.name}: ${e.field} - ${e.message}`);
    // ValidationError: email - Email is required
  } else {
    throw e; // Re-throw unknown errors
  }
}

Error Propagation

When an error is thrown and not caught, it propagates up the call stack until it finds a catch block or crashes the program. You can catch errors at different levels and re-throw them if needed.

Error Propagation and Re-throwing
function level3() {
  throw new Error('Error in level 3');
}

function level2() {
  level3(); // Error propagates up
}

function level1() {
  try {
    level2();
  } catch (e) {
    console.log('Caught at level 1: ' + e.message);
  }
}

level1(); // 'Caught at level 1: Error in level 3'

// Re-throwing: catch only what you can handle
function processData(data) {
  try {
    JSON.parse(data);
  } catch (e) {
    if (e instanceof SyntaxError) {
      console.log('Invalid JSON: ' + e.message);
    } else {
      throw e; // Re-throw unexpected errors
    }
  }
}
processData('not json'); // 'Invalid JSON: ...'

Async Error Handling Basics

Errors in asynchronous code (Promises, async/await) require special handling. Use .catch() for Promises or try...catch inside async functions.

Async Error Handling
// Promise .catch()
function fetchData(shouldFail) {
  return new Promise((resolve, reject) => {
    if (shouldFail) {
      reject(new Error('Fetch failed'));
    } else {
      resolve('data');
    }
  });
}

fetchData(true)
  .then(data => console.log(data))
  .catch(e => console.log('Promise error: ' + e.message));
// 'Promise error: Fetch failed'

// async/await with try...catch
async function getData() {
  try {
    const result = await fetchData(true);
    console.log(result);
  } catch (e) {
    console.log('Async error: ' + e.message);
  }
}
getData(); // 'Async error: Fetch failed'
ConceptUse Case
try...catchHandle errors that might occur in synchronous code
finallyCleanup operations that must always run
throwCreate and signal an error condition
Custom errorsApplication-specific error types with extra info
Re-throwingCatch only errors you can handle, pass others up
.catch()Handle errors in Promise chains
async try...catchHandle errors in async/await code
Exercise:
When does the finally block execute?
Try it YourselfCtrl+Enter to run
Click Run to see the output here.