Ad – 728Γ—90
πŸ”§ 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.

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?

πŸ‹οΈ 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.

Frequently Asked Questions

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.
Ad – 336Γ—280

πŸ“‹ 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.