JavaScript · Syntax · Advanced
Error Handling
Custom errors, try/catch/finally, async errors, and defensive patterns
Quick topic start and explanations before exercises (exercises below):
Async errors, Promise.allSettled, and safeParseJSON
#Result pattern, error handling reference, and common pitfalls
#Exercises:
Implement a custom ValidationError class
#Create a `ValidationError` class that extends `Error`. The constructor takes `message` and `field`. Set `this.name = "ValidationError"` so it distinguishes itself from plain errors. Verify that `instanceof ValidationError` and `instanceof Error` both return `true`.
class ValidationError extends Error {
// add: name='ValidationError', field property
}
// Test:
try {
throw new ValidationError('email is required', 'email');
} catch (err) {
console.log(err.name); // 'ValidationError'
console.log(err.message); // 'email is required'
console.log(err.field); // 'email'
console.log(err instanceof ValidationError); // true
console.log(err instanceof Error); // true
}
Solution
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
try {
throw new ValidationError('email is required', 'email');
} catch (err) {
console.log(err.name); // 'ValidationError'
console.log(err.message); // 'email is required'
console.log(err.field); // 'email'
console.log(err instanceof ValidationError); // true
console.log(err instanceof Error); // true
}
Predict the try/catch/finally execution order
#What is the exact output of this code? Explain why `finally` runs even though `catch` has a `return` statement, and why `"after throw"` is never logged.
function test() {
try {
console.log('try');
throw new Error('oops');
console.log('after throw');
} catch (e) {
console.log('catch:', e.message);
return 'from catch';
} finally {
console.log('finally');
}
}
const result = test();
console.log('result:', result);
Solution
// Output order:
// try
// catch: oops
// finally ← runs before the return from catch lands
// result: from catch
// 'after throw' is never logged because execution jumps to catch
// finally always runs even when catch has a return statement
Handle async errors with try/catch
#Implement `fetchData(url)` as an `async` function. Use `fetch` to make a request. If `res.ok` is false, throw an `Error` that includes the HTTP status code. Catch any error, log `"fetchData failed: <message>"`, and return `null`.
async function fetchData(url) {
// fetch url, throw if !res.ok (include HTTP status in message)
// on error: log the message and return null
}
Solution
async function fetchData(url) {
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP error: ${res.status}`);
return await res.json();
} catch (err) {
console.error('fetchData failed:', err.message);
return null;
}
}
Fetch multiple URLs with Promise.allSettled
#Implement `fetchAll(urls)` that fetches all URLs concurrently and returns `{ succeeded: [...responses], failed: [...errorMessages] }`. Some requests may fail — don't let one failure cancel the others. Use `Promise.allSettled`.
async function fetchAll(urls) {
// fetch all urls concurrently
// return { succeeded: [...responses], failed: [...errorMessages] }
}
Solution
async function fetchAll(urls) {
const results = await Promise.allSettled(urls.map(u => fetch(u)));
const succeeded = [];
const failed = [];
for (const r of results) {
if (r.status === 'fulfilled') succeeded.push(r.value);
else failed.push(r.reason.message);
}
return { succeeded, failed };
}
Selectively handle and re-throw errors
#Complete `handleRequest()` to handle `NetworkError` (log `"retry later"`) and `AuthError` (log `"please log in"`) selectively. Any other error type must be re-thrown so it propagates to the caller.
class NetworkError extends Error {
constructor(msg) { super(msg); this.name = 'NetworkError'; }
}
class AuthError extends Error {
constructor(msg) { super(msg); this.name = 'AuthError'; }
}
function handleRequest() {
try {
riskyOperation(); // may throw NetworkError, AuthError, or Error
} catch (err) {
// handle NetworkError: log 'retry later'
// handle AuthError: log 'please log in'
// anything else: re-throw
}
}
Solution
class NetworkError extends Error {
constructor(msg) { super(msg); this.name = 'NetworkError'; }
}
class AuthError extends Error {
constructor(msg) { super(msg); this.name = 'AuthError'; }
}
function handleRequest() {
try {
riskyOperation();
} catch (err) {
if (err instanceof NetworkError) {
console.log('retry later');
} else if (err instanceof AuthError) {
console.log('please log in');
} else {
throw err; // unexpected — propagate
}
}
}
Implement safeParseJSON with a fallback
#Implement `safeParseJSON(str, fallback = null)` that attempts to parse `str` as JSON. If parsing fails for any reason, return `fallback` instead of throwing. The function must never throw.
function safeParseJSON(str, fallback = null) {
// parse JSON safely, return fallback on invalid input
}
Solution
function safeParseJSON(str, fallback = null) {
try {
return JSON.parse(str);
} catch {
return fallback;
}
}
safeParseJSON('{"name":"Alice"}'); // {name: 'Alice'}
safeParseJSON('invalid json'); // null
safeParseJSON('bad', []); // []
safeParseJSON('bad', { default: true }); // { default: true }
Collect multiple validation errors before throwing
#Implement `AggregateError` (extends `Error`, stores `.errors` array) and `validateUser(data)`. Validate: `name` is non-empty string, `age` is number ≥ 0, `email` contains `"@"`. Collect ALL failures and throw a single `AggregateError` if any exist.
class AggregateError extends Error {
constructor(errors, message) {
// errors is an array of Error objects
// store them on this.errors
}
}
function validateUser(data) {
const errors = [];
// validate: data.name must be non-empty string
// validate: data.age must be a number >= 0
// validate: data.email must contain '@'
// if any errors, throw AggregateError with message 'Validation failed'
}
Solution
class AggregateError extends Error {
constructor(errors, message) {
super(message);
this.name = 'AggregateError';
this.errors = errors;
}
}
function validateUser(data) {
const errors = [];
if (!data.name || typeof data.name !== 'string')
errors.push(new Error('name must be a non-empty string'));
if (typeof data.age !== 'number' || data.age < 0)
errors.push(new Error('age must be a non-negative number'));
if (!data.email || !data.email.includes('@'))
errors.push(new Error('email must contain @'));
if (errors.length) throw new AggregateError(errors, 'Validation failed');
}
try {
validateUser({ name: '', age: -1, email: 'bad' });
} catch (err) {
console.log(err.message); // 'Validation failed'
err.errors.forEach(e => console.log(' -', e.message));
}
Implement the Result pattern for error handling
#Implement `divide(a, b)` and `safeSqrt(n)` using the Result pattern: return `{ ok: true, value }` on success and `{ ok: false, error }` on failure. Neither function should throw. Division by zero and negative square root are error cases.
function divide(a, b) {
// return { ok: true, value } or { ok: false, error }
// do NOT throw
}
function safeSqrt(n) {
// return { ok: true, value } or { ok: false, error }
// n < 0 is an error
}
Solution
function divide(a, b) {
if (b === 0) return { ok: false, error: 'division by zero' };
return { ok: true, value: a / b };
}
function safeSqrt(n) {
if (n < 0) return { ok: false, error: 'cannot take sqrt of negative number' };
return { ok: true, value: Math.sqrt(n) };
}
// Usage:
const r1 = divide(10, 2);
if (r1.ok) console.log(r1.value); // 5
const r2 = divide(10, 0);
if (!r2.ok) console.log(r2.error); // 'division by zero'
const r3 = safeSqrt(-4);
if (!r3.ok) console.log(r3.error); // 'cannot take sqrt of negative number'
Chain errors with the cause option
#Implement `loadConfig(path)` that reads a file with `readFile(path)` and parses it with `JSON.parse`. Catch any error and wrap it: `throw new Error("Failed to load config", { cause: err })`. This preserves the original error on `.cause`.
// Error cause chaining: new Error('msg', { cause: originalErr })
async function loadConfig(path) {
// read file (simulate: may throw 'file not found' Error)
// parse JSON (may throw SyntaxError)
// wrap any error with cause and message 'Failed to load config'
}
// Simulate helpers:
async function readFile(path) {
if (path === 'missing.json') throw new Error('file not found');
return '{"valid": true}';
}
Solution
async function loadConfig(path) {
try {
const text = await readFile(path);
return JSON.parse(text);
} catch (err) {
throw new Error('Failed to load config', { cause: err });
}
}
async function readFile(path) {
if (path === 'missing.json') throw new Error('file not found');
return '{"valid": true}';
}
// Usage:
try {
await loadConfig('missing.json');
} catch (err) {
console.log(err.message); // 'Failed to load config'
console.log(err.cause.message); // 'file not found'
}
Set up global unhandled rejection handlers
#Explain what an unhandled Promise rejection is and when it occurs. Write the global handler registration for both browser (`window`) and Node.js (`process`). The handler should log `"Unhandled rejection: <reason>"`. Demonstrate by creating a rejected Promise that has no `.catch()`.
// In a browser or Node.js environment:
// Set up a global handler for unhandled Promise rejections
// that logs: 'Unhandled rejection: <reason>'
// In browser:
// window.addEventListener('unhandledrejection', handler)
// In Node.js:
// process.on('unhandledRejection', handler)
// Demonstrate by creating a rejected promise with no .catch()
Solution
// Browser:
window.addEventListener('unhandledrejection', (event) => {
console.log('Unhandled rejection:', event.reason);
event.preventDefault(); // prevents browser console error
});
// Node.js:
process.on('unhandledRejection', (reason, promise) => {
console.log('Unhandled rejection:', reason);
});
// Trigger it:
Promise.reject(new Error('forgot to catch me'));
// → 'Unhandled rejection: Error: forgot to catch me'
// Always prefer .catch() or try/catch over relying on this handler —
// this is a last resort to prevent silent failures.