Error Handling
Write resilient code that handles failures gracefully — try/catch, custom errors, and global handlers.
1. Introduction
Errors are inevitable. Networks fail, APIs return unexpected data, users enter invalid input, third-party libraries throw. Error handling is how you stop your application from crashing and instead show the user a helpful message or recover gracefully.
Good error handling is the difference between a professional application and a broken one. This lesson covers try/catch in depth, custom error classes, async error handling, and global error boundaries.
2. Theory
2.1 The Error object
// JavaScript has several built-in error types
const err = new Error('Something went wrong');
console.log(err.name); // 'Error'
console.log(err.message); // 'Something went wrong'
console.log(err.stack); // stack trace string
// Built-in error subtypes
new TypeError('Expected a string'); // wrong type
new RangeError('Value out of range'); // value out of range
new ReferenceError('x is not defined'); // undefined variable
new SyntaxError('Unexpected token'); // invalid JS syntax
new URIError('Malformed URI');
new EvalError();
2.2 try / catch / finally
function divide(a, b) {
if (b === 0) throw new RangeError('Division by zero');
return a / b;
}
try {
const result = divide(10, 0);
console.log(result); // skipped
} catch (err) {
console.error(`${err.name}: ${err.message}`); // "RangeError: Division by zero"
} finally {
console.log('Always runs — cleanup here');
}
// You can throw any value, but Error objects are best (have stack traces)
try {
throw 'plain string error'; // legal but bad practice
} catch (err) {
console.log(typeof err); // 'string'
// no stack trace available
}
2.3 Checking error type in catch
function processInput(input) {
try {
if (typeof input !== 'string') throw new TypeError('Expected a string');
if (!input.trim()) throw new RangeError('Input cannot be empty');
return input.trim().toUpperCase();
} catch (err) {
if (err instanceof TypeError) {
return 'TYPE ERROR: ' + err.message;
} else if (err instanceof RangeError) {
return 'RANGE ERROR: ' + err.message;
}
throw err; // re-throw unknown errors
}
}
console.log(processInput(42)); // "TYPE ERROR: Expected a string"
console.log(processInput('')); // "RANGE ERROR: Input cannot be empty"
console.log(processInput('hi')); // "HI"
2.4 Custom error classes
// Create meaningful domain-specific errors
class NetworkError extends Error {
constructor(message, status) {
super(message);
this.name = 'NetworkError';
this.status = status;
}
}
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
class NotFoundError extends Error {
constructor(resource, id) {
super(`${resource} with id ${id} not found`);
this.name = 'NotFoundError';
this.resource = resource;
this.id = id;
}
}
// Usage — callers can distinguish error types
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
if (response.status === 404) throw new NotFoundError('User', id);
if (!response.ok) throw new NetworkError('Request failed', response.status);
return response.json();
}
try {
const user = await fetchUser(999);
} catch (err) {
if (err instanceof NotFoundError) {
showMessage(`User ${err.id} doesn't exist`);
} else if (err instanceof NetworkError) {
showMessage(`Server error (${err.status}) — try again`);
} else {
throw err; // unexpected — re-throw
}
}
2.5 Async error handling patterns
// Pattern 1: try/catch in async function
async function loadUser(id) {
try {
const user = await fetchUser(id);
return { data: user, error: null };
} catch (err) {
return { data: null, error: err };
}
}
const { data, error } = await loadUser(1);
if (error) { showError(error); } else { render(data); }
// Pattern 2: Result object (inspired by Rust/Go)
async function safe(promise) {
try {
const data = await promise;
return [null, data];
} catch (err) {
return [err, null];
}
}
const [err, user] = await safe(fetchUser(1));
if (err) { console.error(err); } else { console.log(user); }
// Pattern 3: Promise .catch for simple cases
fetchUser(1)
.then(renderUser)
.catch(err => showError(err.message));
2.6 Global error handlers
// Catch unhandled runtime errors (script errors)
window.addEventListener('error', event => {
console.error('Uncaught error:', event.message);
console.error('File:', event.filename, 'Line:', event.lineno);
// Send to error monitoring service (Sentry, Datadog, etc.)
reportError({ message: event.message, stack: event.error?.stack });
event.preventDefault(); // prevents default browser error console
});
// Catch unhandled Promise rejections
window.addEventListener('unhandledrejection', event => {
console.error('Unhandled rejection:', event.reason);
reportError({ message: String(event.reason), type: 'unhandledRejection' });
event.preventDefault(); // suppress browser console warning
});
// These are last-resort safety nets — don't rely on them for normal error handling!
2.7 Error boundaries — contain failures
// Wrap risky operations so one failure doesn't crash everything
function safeRender(renderFn, fallbackHtml) {
try {
return renderFn();
} catch (err) {
console.error('Render error:', err);
return fallbackHtml;
}
}
document.querySelector('#widget').innerHTML = safeRender(
() => renderComplexWidget(data),
'<p>Widget temporarily unavailable.</p>'
);
// Each section renders independently — one broken widget doesn't affect others
2.8 Input validation errors
class FormValidationError extends Error {
constructor(errors) { // errors = { fieldName: 'error message' }
super('Form validation failed');
this.name = 'FormValidationError';
this.errors = errors;
}
}
function validateSignup({ username, email, password }) {
const errors = {};
if (!username || username.length < 3)
errors.username = 'Username must be at least 3 characters';
if (!/\S+@\S+\.\S+/.test(email))
errors.email = 'Please enter a valid email address';
if (password.length < 8)
errors.password = 'Password must be at least 8 characters';
if (Object.keys(errors).length) throw new FormValidationError(errors);
}
try {
validateSignup({ username: 'Al', email: 'bad', password: '123' });
} catch (err) {
if (err instanceof FormValidationError) {
Object.entries(err.errors).forEach(([field, msg]) => {
document.querySelector(`#${field}-error`).textContent = msg;
});
}
}
3. Real World Example
// Resilient fetch with retry, timeout, and structured errors
class HttpError extends Error {
constructor(status, message, data = null) {
super(message);
this.name = 'HttpError';
this.status = status;
this.data = data;
}
}
async function fetchWithRetry(url, options = {}, retries = 3, delay = 1000) {
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 10000); // 10s timeout
const response = await fetch(url, { ...options, signal: controller.signal });
clearTimeout(timer);
if (!response.ok) {
const body = await response.json().catch(() => ({}));
throw new HttpError(response.status, body.message || `HTTP ${response.status}`, body);
}
return await response.json();
} catch (err) {
if (err.name === 'AbortError') throw new Error('Request timed out');
if (err instanceof HttpError && err.status < 500) throw err; // don't retry 4xx
if (attempt === retries) throw err; // last attempt — throw
await new Promise(resolve => setTimeout(resolve, delay * attempt)); // backoff
console.warn(`Attempt ${attempt} failed, retrying...`);
}
}
}
// Usage
async function loadDashboard() {
try {
const data = await fetchWithRetry('/api/dashboard');
render(data);
} catch (err) {
if (err instanceof HttpError) {
if (err.status === 401) redirectToLogin();
else if (err.status === 403) showForbidden();
else showError(`Server error: ${err.message}`);
} else {
showError('Connection problem — check your internet connection');
}
}
}
4. Code Example
<input id="url-in" placeholder="Enter API URL" value="https://jsonplaceholder.typicode.com/users/1">
<button id="fetch-btn">Fetch</button>
<button id="fail-btn">Trigger 404</button>
<div id="result"></div>
<script>
class ApiError extends Error {
constructor(status, message) {
super(message);
this.name = 'ApiError';
this.status = status;
}
}
async function apiFetch(url) {
const response = await fetch(url);
if (!response.ok) {
throw new ApiError(response.status, `HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
const resultEl = document.querySelector('#result');
function showResult(html) { resultEl.innerHTML = html; }
async function fetchAndRender(url) {
showResult('<p>Loading...</p>');
try {
const data = await apiFetch(url);
const pre = document.createElement('pre');
pre.textContent = JSON.stringify(data, null, 2); // safe — textContent
resultEl.innerHTML = '';
resultEl.appendChild(pre);
} catch (err) {
if (err instanceof ApiError) {
let msg;
if (err.status === 404) msg = 'Resource not found (404)';
else if (err.status === 403) msg = 'Access denied (403)';
else if (err.status >= 500) msg = `Server error (${err.status}) — try again`;
else msg = err.message;
showResult(`<p style="color:red">Error: ${msg}</p>`);
} else if (err.name === 'TypeError') {
showResult(`<p style="color:red">Network error — are you offline?</p>`);
} else {
showResult(`<p style="color:red">Unexpected error: ${err.message}</p>`);
throw err; // re-throw truly unexpected errors
}
}
}
document.querySelector('#fetch-btn').addEventListener('click', () => {
fetchAndRender(document.querySelector('#url-in').value.trim());
});
document.querySelector('#fail-btn').addEventListener('click', () => {
fetchAndRender('https://jsonplaceholder.typicode.com/users/9999');
});
</script>
5. Code Breakdown
Custom ApiError class
By extending Error with a custom class, callers can use instanceof ApiError to distinguish HTTP errors from network errors. The status property provides the specific HTTP code for conditional handling.
Layered error handling
The catch block handles three distinct cases: ApiError (HTTP-level issues), TypeError (network-level issues — fetch throws TypeError when the network is unavailable), and unknown errors (re-thrown to surface bugs).
textContent for API data
pre.textContent = JSON.stringify(data, null, 2) — even though the data comes from a trusted API here, using textContent is the safe habit. Never insert API data via innerHTML.
6. Common Mistakes
Mistake 1 — Swallowing errors silently
// Bug — error disappears, user sees nothing
try {
doThing();
} catch (err) {
// empty catch — error swallowed!
}
// Fix — at minimum, log the error
try {
doThing();
} catch (err) {
console.error(err); // log it
showToast('Something went wrong'); // notify user
}
Mistake 2 — Not re-throwing unexpected errors
// Bug — catches errors it doesn't understand and hides them
try {
loadConfig();
} catch (err) {
showError('Config failed'); // handles ALL errors the same
}
// Fix — handle known errors, re-throw unknown ones
try {
loadConfig();
} catch (err) {
if (err instanceof ConfigError) { showError(err.message); }
else { throw err; } // unknown error — let it surface
}
Mistake 3 — try/catch around the wrong thing
// Bug — catch doesn't wrap the actual async operation
try {
const promise = fetch('/api/data'); // fetch starts here, doesn't throw
const data = await promise.json(); // throws here but OUTSIDE try!
} catch (err) {
console.error(err); // may not catch!
}
// Fix — await inside try
try {
const response = await fetch('/api/data');
const data = await response.json();
} catch (err) {
console.error(err); // correctly catches both
}
7. Best Practices
- Use custom error classes to give errors semantic meaning — callers can distinguish types with instanceof.
- Never swallow errors silently — at minimum log them; ideally notify the user.
- Re-throw errors you can't handle — don't pretend everything is fine when it's not.
- Handle errors at the right level — catch where you can recover; let others bubble up.
- Always await inside try/catch — awaiting outside the block means the catch won't trigger.
- Set up global handlers as last-resort safety nets for any uncaught exceptions.
8. Practice Exercise
- Write a
validateAge(age)function that throws a TypeError if age is not a number, a RangeError if it's negative or over 150, and returns the age otherwise. Catch each type distinctly. - Create a
NetworkErrorandAuthErrorcustom class. Write asecureFetch(url)that throws the appropriate one based on status code (401 → AuthError, others → NetworkError). - Add a global
unhandledrejectionlistener that logs to a visible#error-logdiv in the page. Intentionally trigger an unhandled rejection to test it.
9. Assignment
Build a "Resilient API Explorer" page.
- Create custom error classes:
NetworkError,HttpError(with status),ParseError,TimeoutError. - Write a
robustFetch(url, options)function that: times out after 8 seconds, retries 3 times on 5xx errors (with exponential backoff), throws typed errors for each failure mode. - Build a UI: URL input, "Fetch" button. Display the result (pretty-printed JSON) or a coloured error message depending on the error type.
- Add a global unhandledrejection handler that logs to a visible "Error Log" panel at the bottom.
Deliverable: One HTML file.
10. Interview Questions
- What is a try/catch block?
Code inside try is executed normally. If any statement throws, execution jumps to catch where the error is received as a parameter. finally always runs after try or catch — used for cleanup regardless of outcome. - How do you create a custom error class?
Extend the Error class: class MyError extends Error { constructor(msg) { super(msg); this.name = 'MyError'; } }. Callers can then use instanceof MyError to distinguish it from other errors. - What does re-throwing an error mean and when should you do it?
Re-throwing means catching an error and then throwing it again (throw err). Do it when the current code can't meaningfully handle the error — pass it up to a caller that can. Only catch errors you know how to handle. - What is the unhandledrejection event?
A window event that fires when a Promise is rejected and no .catch() or try/catch handles it. Adding a listener is useful as a last-resort safety net for logging unexpected errors before they silently disappear.
11. Additional Resources
- MDN — Error
- MDN — try...catch
- javascript.info — Error handling with promises
- MDN — Window: unhandledrejection event