Advertisement
πŸ”§ Intermediate JS

JavaScript Error Handling – try, catch, finally

Errors are inevitable. How you handle them determines whether your application crashes silently or fails gracefully with a useful message. Mastering try/catch/finally, custom error types, and async error handling is essential for writing production-quality JavaScript.

⏱️ 24 min read 🎯 Intermediate πŸ“… Updated 2026

try / catch / finally

The try block contains code that might throw. If an exception occurs, execution jumps to catch. The finally block always runs β€” even if an error was thrown or a return statement was hit.

JavaScript
function parseJSON(text) {
  try {
    const data = JSON.parse(text);
    console.log('Parsed:', data);
    return data;
  } catch (error) {
    console.error('Parse failed:', error.message);
    return null;
  } finally {
    console.log('parseJSON completed'); // always runs
  }
}

parseJSON('{"name":"Alice"}'); // Parsed: {name:'Alice'} β†’ parseJSON completed
parseJSON('not valid json');   // Parse failed: ... β†’ parseJSON completed

// Optional catch binding (ES2019) – omit the error variable if not needed
try {
  riskyOperation();
} catch {
  console.log('Something failed');
}

// You can throw anything
try {
  throw 'a string error'; // valid but not recommended
  throw 42;               // also valid
  throw { code: 500 };    // better – but use Error objects for best practice
} catch (e) {
  console.log(typeof e, e);
}

The Error Object

JavaScript
try {
  null.property; // throws TypeError
} catch (error) {
  console.log(error.name);    // 'TypeError'
  console.log(error.message); // "Cannot read properties of null"
  console.log(error.stack);   // Stack trace (string with file + line info)
  console.log(error instanceof TypeError); // true
  console.log(error instanceof Error);     // true
}

// Creating Error objects explicitly
const err = new Error('Something went wrong');
console.log(err.name);    // 'Error'
console.log(err.message); // 'Something went wrong'
console.log(err.stack);   // Error: Something went wrong\n  at ...

// throw new Error is always preferred over throw 'string'
// because it includes a stack trace
β–Ά Output
TypeError
Cannot read properties of null (reading 'property')
[stack trace...]
true
true

Built-in Error Types

TypeWhen it occursExample
ErrorBase class / genericthrow new Error('...')
TypeErrorWrong type usednull.property, calling a non-function
ReferenceErrorVariable not definedconsole.log(undeclared)
SyntaxErrorInvalid JavaScript syntaxJSON.parse('bad'), parsing malformed code
RangeErrorValue out of allowed rangenew Array(-1), n.toFixed(200)
URIErrorMalformed URIdecodeURIComponent('%')
EvalErrorMisuse of eval()Rarely encountered in modern code

Custom Error Classes

Extend the built-in Error class to create domain-specific error types. This lets you write catch blocks that handle only specific error types.

JavaScript
// Custom error class
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;
  }
}

// Using custom errors
function validateAge(age) {
  if (typeof age !== 'number') {
    throw new ValidationError('Age must be a number', 'age');
  }
  if (age < 0 || age > 150) {
    throw new ValidationError('Age must be between 0 and 150', 'age');
  }
  return age;
}

try {
  validateAge(-5);
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(`Validation failed on field "${error.field}": ${error.message}`);
  } else {
    throw error; // re-throw unknown errors
  }
}
// Validation failed on field "age": Age must be between 0 and 150

Re-throwing Errors

JavaScript
// Only handle errors you understand β€” re-throw everything else
function processFile(data) {
  try {
    return JSON.parse(data);
  } catch (error) {
    if (error instanceof SyntaxError) {
      // We understand this β€” handle it gracefully
      console.warn('Invalid JSON, using empty object');
      return {};
    }
    // Unknown error β€” re-throw to propagate it up
    throw error;
  }
}

// Wrapping and re-throwing with context
async function loadUserProfile(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) {
      throw new NetworkError(`Failed to load user ${userId}`, response.status);
    }
    return await response.json();
  } catch (error) {
    if (error instanceof NetworkError) throw error; // already wrapped
    throw new NetworkError(`Unexpected error loading user: ${error.message}`, 0);
  }
}
πŸ’‘
The Golden Rule of Error Handling

Only catch errors you know how to handle. If you catch an error just to log it and can't recover, re-throw it. Silently swallowing unknown errors leads to mysterious bugs that are extremely hard to diagnose.

Error Handling in Async Code

JavaScript
// async/await – use try/catch exactly like synchronous code
async function fetchData(url) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new NetworkError(`HTTP ${response.status}`, response.status);
    }
    const data = await response.json();
    return data;
  } catch (error) {
    if (error instanceof NetworkError) {
      console.error('Network error:', error.message);
      return null;
    }
    throw error;
  }
}

// Promise chain – use .catch()
fetch('/api/data')
  .then(res => res.json())
  .then(data => processData(data))
  .catch(error => {
    console.error('Pipeline failed:', error);
  })
  .finally(() => {
    hideLoadingSpinner();
  });

// Promise.allSettled – wait for all, even if some fail
const results = await Promise.allSettled([
  fetch('/api/users'),
  fetch('/api/products'),
  fetch('/api/orders')
]);

results.forEach((result, i) => {
  if (result.status === 'fulfilled') {
    console.log(`Request ${i} succeeded`);
  } else {
    console.error(`Request ${i} failed:`, result.reason);
  }
});

Global Error Handlers

JavaScript
// Browser: catch uncaught synchronous errors
window.onerror = function(message, source, lineno, colno, error) {
  sendToErrorService({ message, source, lineno, error: error?.stack });
  return true; // prevents default browser error display
};

// Browser: catch unhandled promise rejections
window.addEventListener('unhandledrejection', event => {
  console.error('Unhandled promise rejection:', event.reason);
  sendToErrorService({ type: 'unhandledrejection', reason: event.reason });
  event.preventDefault(); // prevents console output in some environments
});

// Node.js equivalent
process.on('uncaughtException', (error) => {
  console.error('Uncaught exception:', error);
  // Clean up and exit β€” DO NOT continue running after uncaughtException
  process.exit(1);
});

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

// Error logging best practice
function reportError(error, context = {}) {
  const report = {
    name: error.name,
    message: error.message,
    stack: error.stack,
    timestamp: new Date().toISOString(),
    url: window.location.href,
    ...context
  };
  // Send to Sentry, Datadog, etc.
  navigator.sendBeacon('/api/errors', JSON.stringify(report));
}
πŸ’‘
Always Handle Promise Rejections

Every async function call and Promise chain should have error handling. In Node.js 15+, unhandled promise rejections crash the process. In browsers, they generate console warnings. Use Promise.allSettled when you want all promises to complete regardless of failures.

try/catch Done Right β€” and the Anti-Pattern to Avoid

try/catch/finally handles runtime errors. Throw real Error objects (not strings) so you get a name, message, and stack trace. finally runs whether or not an error occurred β€” perfect for cleanup.

try {
  const data = JSON.parse(input);   // may throw SyntaxError
} catch (err) {
  console.error(err.name, err.message);   // structured, has a stack
} finally {
  closeConnection();                // always runs
}

throw new Error("Not found");       // βœ… throw Error objects, not "strings"

async needs try/catch too

A rejected promise inside async behaves like a thrown error β€” wrap the await:

async function load() {
  try { return await fetch(url).then(r => r.json()); }
  catch (err) { showToast("Load failed"); throw err; }  // handle, then re-throw
}

The anti-pattern: the empty catch β€” catch (e) {}. Swallowing an error hides the bug and leaves the program in a broken state with no clue why. If you catch it, do something: log it, recover, or re-throw. Also make a custom error class (class ValidationError extends Error) when callers need to tell error types apart.

πŸ‹οΈ Practical Exercise

Create a safeFetch(url, options) utility function that wraps fetch and:

  1. Throws a custom NetworkError with the HTTP status code for non-2xx responses.
  2. Throws a custom ParseError if the response body is not valid JSON.
  3. Automatically retries up to 3 times on network failures (but not on 4xx errors).
  4. Returns the parsed JSON on success.

πŸ”₯ Challenge Exercise

Build an error boundary utility for async operations. Write a function withErrorBoundary(fn, fallback, onError) that wraps any async function: if it throws, calls onError(error) for logging and returns fallback instead of propagating the error. Then create a higher-order version createBoundedFn(fn, options) that adds retry logic, timeout, and error categorization.

Advertisement

πŸ“‹ Summary

  • try/catch/finally: try runs code, catch handles errors, finally always runs.
  • Always throw Error objects (not strings) to get a stack trace.
  • Use error.name, error.message, and error.stack for diagnostics.
  • Create custom error classes by extending Error for domain-specific error types.
  • Only catch errors you can handle β€” re-throw everything else.
  • Use try/catch inside async functions to handle async errors synchronously.
  • Every Promise chain needs a .catch(); every await should be in a try/catch.
  • Register global handlers (unhandledrejection) to catch any escaped errors and report them.

Interview Questions

  • Does finally run if a return statement is inside try?
  • What is the difference between error.name and instanceof for type checking?
  • Why should you re-throw errors you cannot handle?
  • How do you catch errors in an async function?
  • What is an unhandled promise rejection?
  • What is the advantage of custom error classes over throwing plain strings?

FAQ

Does finally always execute? +

Yes β€” except when the entire JavaScript engine halts (e.g., process.exit()) or an infinite loop blocks execution. Even if try or catch contains a return statement, finally runs first. If finally itself contains a return, it overrides the value from try/catch.

Should I use error instanceof TypeError or error.name === 'TypeError'? +

instanceof is generally safer and more idiomatic. However, across iframes or different execution contexts, instanceof can give false negatives (because each frame has its own Error prototype). For cross-realm code, error.name === 'TypeError' is more reliable.

Can I throw any value in JavaScript? +

Yes β€” you can throw any value (string, number, object). However, always use throw new Error() or a subclass. Only Error objects include a stack trace, which is essential for debugging in production.

What is the difference between Promise.all and Promise.allSettled? +

Promise.all rejects immediately if any promise rejects. Promise.allSettled waits for all promises to complete, regardless of success or failure, and returns an array of result objects with status: 'fulfilled' or status: 'rejected'.

How do error boundaries work in React? +

React's error boundaries are class components that implement componentDidCatch. They catch errors thrown during rendering, lifecycle methods, and constructors of child components β€” but not errors in event handlers or async code. For those, use regular try/catch.