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.
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
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
TypeError Cannot read properties of null (reading 'property') [stack trace...] true true
Built-in Error Types
| Type | When it occurs | Example |
|---|---|---|
Error | Base class / generic | throw new Error('...') |
TypeError | Wrong type used | null.property, calling a non-function |
ReferenceError | Variable not defined | console.log(undeclared) |
SyntaxError | Invalid JavaScript syntax | JSON.parse('bad'), parsing malformed code |
RangeError | Value out of allowed range | new Array(-1), n.toFixed(200) |
URIError | Malformed URI | decodeURIComponent('%') |
EvalError | Misuse 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.
// 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
// 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);
}
}
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
// 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
// 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));
}
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
finallyrun if areturnstatement is insidetry? - What is the difference between
error.nameandinstanceoffor 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:
- Throws a custom
NetworkErrorwith the HTTP status code for non-2xx responses. - Throws a custom
ParseErrorif the response body is not valid JSON. - Automatically retries up to 3 times on network failures (but not on 4xx errors).
- 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
finallyalways execute? - Yes β except when the entire JavaScript engine halts (e.g.,
process.exit()) or an infinite loop blocks execution. Even iftryorcatchcontains areturnstatement,finallyruns first. Iffinallyitself contains areturn, it overrides the value fromtry/catch. - Should I use
error instanceof TypeErrororerror.name === 'TypeError'? instanceofis generally safer and more idiomatic. However, across iframes or different execution contexts,instanceofcan give false negatives (because each frame has its ownErrorprototype). For cross-realm code,error.name === 'TypeError'is more reliable.- Can I throw any value in JavaScript?
- Yes β you can
throwany value (string, number, object). However, always usethrow new Error()or a subclass. OnlyErrorobjects include astacktrace, which is essential for debugging in production. - What is the difference between
Promise.allandPromise.allSettled? Promise.allrejects immediately if any promise rejects.Promise.allSettledwaits for all promises to complete, regardless of success or failure, and returns an array of result objects withstatus: 'fulfilled'orstatus: '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.
π Summary
try/catch/finally: try runs code, catch handles errors, finally always runs.- Always throw
Errorobjects (not strings) to get a stack trace. - Use
error.name,error.message, anderror.stackfor diagnostics. - Create custom error classes by extending
Errorfor domain-specific error types. - Only catch errors you can handle β re-throw everything else.
- Use
try/catchinsideasyncfunctions to handle async errors synchronously. - Every Promise chain needs a
.catch(); everyawaitshould be in atry/catch. - Register global handlers (
unhandledrejection) to catch any escaped errors and report them.