JavaScript · Syntax · Intermediate
Custom errors and error handling
Built-in error types, custom Error subclasses, and structured error handling patterns.
Quick topic start and explanations before exercises (exercises below):
try/catch/finally, re-throwing, and unhandled promise rejections
#Error handling strategies: throw vs Result pattern, Error.cause
#Exercises:
Write a function that safely divides two numbers. If the divisor is 0, throw a `RangeError` with message 'Division by zero'.
function safeDivide(a, b) {
// your code here
}
console.log(safeDivide(10, 2)); // 5
try {
safeDivide(10, 0);
} catch (e) {
console.log(e instanceof RangeError); // true
console.log(e.message); // 'Division by zero'
}
Solution
function safeDivide(a, b) {
if (b === 0) throw new RangeError('Division by zero');
return a / b;
}
Create a custom error class `ValidationError` that extends `Error`. It should accept a field name and message, storing them as properties.
class ValidationError extends Error {
constructor(field, message) {
// your code here
}
}
const e = new ValidationError('email', 'Invalid format');
console.log(e instanceof ValidationError); // true
console.log(e instanceof Error); // true
console.log(e.field); // 'email'
console.log(e.message); // 'Invalid format'
console.log(e.name); // 'ValidationError'
Solution
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
Write a `try/catch/finally` block that parses a JSON string. If parsing fails, return a default value `{}`. The `finally` block should log 'parse attempted'.
function safeParse(str) {
// your code here
}
console.log(safeParse('{"a":1}')); // { a: 1 }
console.log(safeParse('invalid')); // {}
// 'parse attempted' should be logged in both cases
Solution
function safeParse(str) {
try {
return JSON.parse(str);
} catch (e) {
return {};
} finally {
console.log('parse attempted');
}
}
Write a function that takes an array and an index. If the index is out of bounds, throw a `RangeError`. If the argument is not an array, throw a `TypeError`.
function getElement(arr, index) {
// your code here
}
console.log(getElement([1, 2, 3], 1)); // 2
try { getElement('not array', 0); } catch(e) { console.log(e.constructor.name); } // TypeError
try { getElement([1,2], 5); } catch(e) { console.log(e.constructor.name); } // RangeError
Solution
function getElement(arr, index) {
if (!Array.isArray(arr)) throw new TypeError('Expected an array');
if (index < 0 || index >= arr.length) throw new RangeError(`Index ${index} out of bounds`);
return arr[index];
}
Write a function that catches a specific error type and re-throws any other errors it doesn't know how to handle.
class NetworkError extends Error {
constructor(msg) { super(msg); this.name = 'NetworkError'; }
}
function handleRequest(fn) {
// Call fn(). If it throws NetworkError, return 'network error handled'.
// If it throws anything else, re-throw it.
}
console.log(handleRequest(() => { throw new NetworkError('timeout'); }));
// 'network error handled'
try {
handleRequest(() => { throw new TypeError('bad type'); });
} catch (e) {
console.log(e instanceof TypeError); // true
}
Solution
class NetworkError extends Error {
constructor(msg) { super(msg); this.name = 'NetworkError'; }
}
function handleRequest(fn) {
try {
return fn();
} catch (e) {
if (e instanceof NetworkError) return 'network error handled';
throw e;
}
}
Handle async errors: write an async function that fetches a URL (simulated). If the fetch fails, log the error message and return null.
async function simulatedFetch(url) {
if (!url.startsWith('https')) throw new Error(`Invalid URL: ${url}`);
return { status: 200, data: 'ok' };
}
async function safeFetch(url) {
// your code here
}
safeFetch('https://example.com').then(console.log); // { status: 200, data: 'ok' }
safeFetch('http://bad.com').then(console.log); // null (and logs the error)
Solution
async function safeFetch(url) {
try {
return await simulatedFetch(url);
} catch (e) {
console.error(e.message);
return null;
}
}
Write a function that wraps another function and catches any TypeError, returning a default value instead. Other error types should propagate normally.
function withTypeErrorFallback(fn, defaultValue) {
// your code here
}
const safe = withTypeErrorFallback(() => null.length, 0);
console.log(safe); // 0
try {
withTypeErrorFallback(() => { throw new RangeError('oops'); }, 0);
} catch (e) {
console.log(e instanceof RangeError); // true
}
Solution
function withTypeErrorFallback(fn, defaultValue) {
try {
return fn();
} catch (e) {
if (e instanceof TypeError) return defaultValue;
throw e;
}
}
Create an `ErrorRegistry` class that records all errors thrown during batch processing. It should collect them instead of stopping at the first one.
class ErrorRegistry {
constructor() {
this.errors = [];
}
run(fn) {
// Call fn(). If it throws, record the error. Never re-throw.
}
hasErrors() { return this.errors.length > 0; }
getErrors() { return this.errors; }
}
const reg = new ErrorRegistry();
reg.run(() => { /* ok */ });
reg.run(() => { throw new Error('first'); });
reg.run(() => { throw new TypeError('second'); });
console.log(reg.hasErrors()); // true
console.log(reg.getErrors().length); // 2
Solution
class ErrorRegistry {
constructor() {
this.errors = [];
}
run(fn) {
try {
fn();
} catch (e) {
this.errors.push(e);
}
}
hasErrors() { return this.errors.length > 0; }
getErrors() { return this.errors; }
}
Implement error chaining: create a function that wraps a low-level error in a higher-level one, preserving the original error as `cause`.
function readConfig(filename) {
throw new Error(`File not found: ${filename}`);
}
function loadApp(configPath) {
// Try readConfig(configPath). If it fails, throw a new Error
// 'Failed to load application' with the original as 'cause'
}
try {
loadApp('config.json');
} catch (e) {
console.log(e.message); // 'Failed to load application'
console.log(e.cause.message); // 'File not found: config.json'
}
Solution
function readConfig(filename) {
throw new Error(`File not found: ${filename}`);
}
function loadApp(configPath) {
try {
return readConfig(configPath);
} catch (e) {
throw new Error('Failed to load application', { cause: e });
}
}
Write an `assertType` function that throws a `TypeError` with a descriptive message if a value is not of the expected type (checked with `typeof`).
function assertType(value, expectedType) {
// your code here
}
assertType(42, 'number'); // ok, no throw
assertType('hello', 'string'); // ok
try {
assertType(42, 'string');
} catch (e) {
console.log(e instanceof TypeError); // true
console.log(e.message); // e.g. 'Expected string, got number'
}
Solution
function assertType(value, expectedType) {
const actual = typeof value;
if (actual !== expectedType) {
throw new TypeError(`Expected ${expectedType}, got ${actual}`);
}
}