10 questions foundCode inside try runs normally; if it throws an error, execution immediately jumps to the catch block instead of crashing the whole program, letting you handle the error gracefully.
try {
JSON.parse('not valid json');
} catch (err) {
console.log('Failed to parse:', err.message);
}
Real-world example Safely parsing user-provided JSON input without crashing the app on malformed data.
Common follow-ups: What runs if there's no error at all — does catch still execute?
JSON & Data Serialization
finally always runs after try/catch, whether or not an error was thrown or caught, and even if the try block returns early — making it ideal for cleanup code that must always execute.
function readFile() {
openFile();
try {
return process();
} finally {
closeFile(); // always runs
}
}
Real-world example Ensuring a loading spinner is hidden whether an API call succeeds or fails.
Common follow-ups: Does a return inside finally override a return from try?
Async Iterators & Streams
new Error('message') creates an object with a stack trace and standard .message/.name properties, which DevTools and error-tracking tools understand. Throwing a plain string or object loses the stack trace, making debugging much harder.
throw new Error('Invalid input'); // has a stack trace
throw 'Invalid input'; // no stack trace, harder to debug
Real-world example Always throwing proper Error instances (or subclasses) so production error-tracking tools like Sentry capture full context.
Common follow-ups: How do you create a custom error type by extending Error?
Classes & Class Syntax
Extend the built-in Error class, call super(message) in the constructor to set up the message and stack trace, then add any extra properties specific to your error type.
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
throw new ValidationError('Required', 'email');
Real-world example Distinguishing a ValidationError from a NetworkError in a catch block to show different UI messages.
Common follow-ups: Why do you need to explicitly set this.name in a custom error subclass?
Classes & Class Syntax
With .then()/.catch() chains, attach a .catch() at the end to handle rejections from any prior step. With async/await, wrap the awaited calls in a try/catch block — both approaches ultimately catch the same underlying rejected Promise.
// Promise chain
fetchData().then(process).catch(err => console.error(err));
// async/await
try {
const data = await fetchData();
process(data);
} catch (err) {
console.error(err);
}
Real-world example Handling a failed API call gracefully instead of an unhandled promise rejection crashing the app.
Common follow-ups: What happens to errors thrown inside a .then() callback — are they caught by a later .catch()?
Promises & async/await
If a function doesn't catch an error itself, it automatically propagates up the call stack to the nearest enclosing try/catch (or crashes the program/rejects the promise if none exists) — you don't need to manually re-throw at every level.
function inner() { throw new Error('boom'); }
function outer() { inner(); } // doesn't need its own try/catch
try {
outer();
} catch (err) {
console.log('Caught:', err.message); // 'Caught: boom'
}
Real-world example Letting a deeply nested validation function throw, and catching it once at the top-level request handler.
Common follow-ups: What is the 'error boundary' pattern, and how does it relate to this in UI frameworks?
Design Patterns in JavaScript
The { cause } option on the Error constructor lets you wrap a lower-level error inside a higher-level, more meaningful one while preserving the original error for debugging — avoiding the common trade-off of losing context vs. re-throwing raw low-level errors.
try {
await fetchUser(id);
} catch (err) {
throw new Error('Failed to load user profile', { cause: err });
}
Real-world example Wrapping a low-level 'fetch failed' network error with a higher-level, user-facing 'Failed to load profile' error while keeping the original for logs.
Common follow-ups: How do you access the original error from the cause property later?
Networking: Fetch XHR WebSockets & CORS
window.addEventListener('error', ...) catches uncaught synchronous exceptions bubbling up to the top; window.addEventListener('unhandledrejection', ...) catches Promise rejections that were never handled by a .catch(). Together they form a last-resort safety net for logging errors that slipped past local handling.
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled rejection:', event.reason);
event.preventDefault(); // suppress default browser logging
});
Real-world example Reporting any error that slips through the app's normal handling to a monitoring service like Sentry.
Common follow-ups: Should you rely on these global handlers instead of local try/catch blocks?
Promises & async/await
The try/catch block finishes executing (and its stack frame is gone) long before the setTimeout callback actually runs on a later turn of the event loop — by the time the error is thrown, there's no enclosing try/catch left to catch it.
try {
setTimeout(() => { throw new Error('too late!'); }, 100);
} catch (err) {
// never runs — the throw happens asynchronously, after this try/catch exits
}
Real-world example A confusing bug where a developer expects a try/catch to catch an error from an async callback, but it silently escapes to the console instead.
Common follow-ups: Where should the try/catch actually go to catch an error thrown inside a setTimeout callback?
Event Loop & Concurrency
Wrap the call in a loop with a try/catch; on failure, wait an increasing delay (e.g. exponential backoff) before retrying, up to a maximum attempt count, then re-throw the final error if all attempts fail.
async function retry(fn, attempts = 3, delay = 500) {
for (let i = 0; i < attempts; i++) {
try {
return await fn();
} catch (err) {
if (i === attempts - 1) throw err;
await new Promise(r => setTimeout(r, delay * 2 ** i));
}
}
}
Real-world example Retrying a flaky network request up to three times with increasing delays before giving up and showing an error.
Common follow-ups: How would you avoid retrying on errors that are clearly not transient, like a 404?
Promises & async/await