π‘οΈ Try/Catch Blocks & Error Handling
Code fails. Networks drop, users type nonsense into forms, files go missing. The difference between a professional application and a fragile one isn't that the professional code never hits an error β it's that it expects errors and handles them gracefully instead of crashing. Today you learn JavaScript's built-in safety net: try, catch, and finally.
Week 2 · Day 5 (Friday: Error Handling and Debugging) · Lecture 1
π― Learning Objectives
By the end of this lesson, you will be able to:
- Explain what an
Errorobject is and name the built-in error types - Wrap risky code in
try/catchand read thename,message, andstackof a caught error - Use a
finallyblock to run cleanup code no matter what happens throwyour own errors and define customErrorsubclasses for domain-specific failures- Handle errors in asynchronous code with
try/catchplusawaitand with.catch()on promises - Apply best-practice patterns: specific catches, no silent swallowing, meaningful messages
Estimated Time: 60 minutes
Practice: Build a safeJSONParse helper and a retry-with-backoff wrapper.
In This Lesson
Why Error Handling Matters
Imagine you're driving and the engine starts making a strange noise. You'd much rather have a dashboard warning light β something that tells you what's wrong and lets you pull over safely β than have the car simply stop dead in the fast lane. Error handling is that dashboard light for your code: it lets your program notice a problem, respond to it, and keep the rest of the application running.
Without it, a single unexpected value can throw an uncaught exception that halts the current call stack. In the browser that can leave a half-rendered, frozen page; on a Node.js server it can crash the entire process and take every other user's request down with it. Handling errors deliberately is what turns "it works on my machine" into "it works in production."
π Errors vs. exceptions
An Error is an object that describes what went wrong. Throwing is the act of raising one, which interrupts normal flow. An exception is a thrown error that is travelling up the call stack looking for a catch. If nothing catches it, it becomes an uncaught exception.
The Error Object & Its Types
When something goes wrong, JavaScript creates an Error object. Every error carries three especially useful properties:
nameβ the category, e.g."TypeError"messageβ a human-readable description of what happenedstackβ the call stack at the moment the error was created (invaluable for debugging)
The language ships with a family of specialized error types, all descending from the base Error:
| Type | Thrown when⦠| Typical example |
|---|---|---|
SyntaxError | the engine can't parse the code | JSON.parse("{bad}") |
ReferenceError | you use a name that doesn't exist | console.log(missing) |
TypeError | a value is not the type an operation needs | null.foo |
RangeError | a number is outside an allowed range | new Array(-1) |
URIError | a URI-handling function gets malformed input | decodeURIComponent("%") |
EvalError | legacy β rarely seen in modern code | (historical) |
You can create one yourself and inspect it:
const err = new TypeError("Expected a number");
console.log(err.name); // "TypeError"
console.log(err.message); // "Expected a number"
console.log(err.stack); // "TypeError: Expected a number\n at ..."
// Every specific type is an Error:
console.log(err instanceof TypeError); // true
console.log(err instanceof Error); // true
The try/catch Statement
The try/catch statement lets you "try" a block of code and "catch" any error it throws, instead of letting that error crash the program. The catch block receives the error object so you can respond intelligently.
try {
// Code that might throw
riskyOperation();
} catch (error) {
// Runs ONLY if something in try threw
console.error("An error occurred:", error.message);
}
// Execution continues here either way
How it flows
The key insight: the moment a line inside try throws, JavaScript abandons the rest of the try block and jumps to catch. Lines after the failing one never run.
Example 1: catching a ReferenceError
function greetUser(name) {
try {
console.log(message); // 'message' was never declared β ReferenceError
return `Hello, ${name}!`;
} catch (error) {
console.error("Caught:", error.name); // "ReferenceError"
console.error("Details:", error.message); // "message is not defined"
return `Hello, ${name}! (recovered)`; // graceful fallback
}
}
console.log(greetUser("Alice"));
// Caught: ReferenceError
// Details: message is not defined
// Hello, Alice! (recovered)
Example 2: catching a TypeError with a safe default
function calculateTotal(items) {
try {
// Fails if items isn't an array of { price } objects
return items.reduce((sum, item) => sum + item.price, 0);
} catch (error) {
console.error("Could not total items:", error.message);
return 0; // a sensible fallback the caller can rely on
}
}
console.log(calculateTotal([{ price: 10 }, { price: 20 }])); // 30
console.log(calculateTotal(null)); // 0 (no crash)
console.log(calculateTotal("not an array")); // 0 (no crash)
π‘ Optional catch binding
If you don't need the error object, ES2019 lets you omit the parameter entirely: try { β¦ } catch { β¦ }. Use it sparingly β usually you do want to at least log the error.
The finally Block
Sometimes code must run whether or not an error occurred β closing a file, hiding a loading spinner, releasing a database connection. That's what finally is for. It runs after try (and after catch, if one fired), on every path out of the block, even a return.
function processFile(filename) {
let file;
try {
file = openFile(filename);
const data = readFile(file); // might throw
return process(data); // success path
} catch (error) {
console.error("Error processing file:", error.message);
return "Failed"; // failure path
} finally {
// Runs on BOTH paths above β perfect for cleanup
if (file) {
closeFile(file);
console.log("File closed");
}
}
}
β οΈ finally always wins
If finally contains its own return, it overrides any return or throw from the try/catch. That's almost always a bug β keep finally for cleanup, not for producing the function's result.
Throwing & Custom Errors
You don't have to wait for JavaScript to throw β you can raise your own error the instant you detect an invalid state with the throw keyword. Always throw an Error object (not a bare string) so callers get a name, message, and stack.
function divide(a, b) {
if (b === 0) {
// β throw "cannot divide by zero"; // loses the stack trace
throw new Error(`Cannot divide ${a} by zero`); // β
}
return a / b;
}
Custom error classes
For real applications, define your own Error subclasses. They read like documentation and let callers branch on the kind of failure with instanceof.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError"; // so error.name is meaningful
}
}
class AuthError extends Error {
constructor(message) {
super(message);
this.name = "AuthError";
}
}
function validateUser(user) {
if (!user) throw new ValidationError("User object is required");
if (!user.email) throw new ValidationError("Email is required");
if (!user.email.includes("@")) throw new ValidationError("Invalid email format");
return true;
}
try {
validateUser({ email: "invalid-email" });
} catch (error) {
if (error instanceof ValidationError) {
console.log("Please fix your input:", error.message);
} else {
throw error; // not ours β let it bubble up
}
}
// Please fix your input: Invalid email format
β Re-throwing what you can't handle
A catch block should only swallow errors it actually knows how to recover from. For anything else, throw error; again so a higher layer (or your global handler) can deal with it. Silently absorbing unknown errors hides real bugs.
Errors in Asynchronous Code
Here's a trap that catches every beginner: a plain try/catch cannot catch an error that happens later, inside a callback or an un-awaited promise. The try block finishes long before the async work fails. There are two correct ways to handle async errors.
1. try/catch with async/await
Because await pauses the function until the promise settles, a rejected promise throws right at the await line β so an ordinary try/catch around it works perfectly.
async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
// fetch only rejects on network failure β a 404/500 is still "ok: false"
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const user = await response.json();
if (!user.email) throw new ValidationError("User data missing email");
return user;
} catch (error) {
if (error instanceof ValidationError) {
console.error("Bad data:", error.message);
return null;
}
if (error instanceof TypeError) {
console.error("Network problem:", error.message);
return null;
}
throw error; // unknown β re-throw
}
}
β οΈ fetch doesn't reject on HTTP errors
A common surprise: fetch() resolves successfully even for 404 or 500 responses. It only rejects when the request can't be made at all (offline, DNS failure, CORS). Always check response.ok yourself and throw if it's false.
2. .catch() on a promise chain
If you're using .then() instead of await, attach a .catch() at the end. One .catch() handles a rejection from any step above it in the chain.
function fetchData(url) {
return fetch(url)
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
})
.then(data => {
if (!data.isValid) throw new ValidationError("Invalid data received");
return data;
})
.catch(error => {
// Catches a throw from EITHER .then above
console.error("Request failed:", error.message);
return { error: error.message }; // recover with a fallback value
});
}
Global Error Handlers
Even careful code misses something. A top-level safety net lets you log unexpected failures (to a monitoring service like Sentry) and show the user a friendly message instead of a broken page.
// Browser: catch synchronous errors that escaped every try/catch
window.addEventListener("error", (event) => {
console.error("Uncaught error:", event.message, "at", event.filename);
reportToService(event.error);
});
// Browser: catch promises that rejected with no .catch()
window.addEventListener("unhandledrejection", (event) => {
console.error("Unhandled rejection:", event.reason);
reportToService(event.reason);
event.preventDefault(); // stop the default console spam
});
On the server, Node.js exposes the equivalent hooks. Note that after an uncaughtException the process is in an unknown state β log it and exit, don't try to keep running.
// Node.js
process.on("unhandledRejection", (reason) => {
console.error("Unhandled rejection:", reason);
});
process.on("uncaughtException", (error) => {
console.error("Uncaught exception:", error);
process.exit(1); // restart cleanly via a process manager
});
π‘ Global handlers are a net, not a plan
Treat these as last-resort logging, not as your primary strategy. Handle errors close to where they happen β the global handler is for the ones you genuinely didn't anticipate.
Practice & Quiz
ποΈ Exercise 1: A safe JSON parser
Goal: JSON.parse throws a SyntaxError on bad input. Write safeJSONParse(text, fallback) that returns the parsed value on success, or fallback (default null) on failure β without ever crashing.
function safeJSONParse(text, fallback = null) {
// TODO: try to parse; on failure log a warning and return fallback
}
console.log(safeJSONParse('{"name":"John"}')); // { name: "John" }
console.log(safeJSONParse("not json")); // null
console.log(safeJSONParse("nope", {})); // {}
π‘ Hint
Wrap JSON.parse(text) in a try. In the catch, log the error's message and return fallback.
β Solution
function safeJSONParse(text, fallback = null) {
try {
return JSON.parse(text);
} catch (error) {
console.warn("Invalid JSON, using fallback:", error.message);
return fallback;
}
}
ποΈ Exercise 2: Retry with backoff
Goal: Write retry(operation, maxAttempts) that runs an async operation, retries on failure up to maxAttempts times with a short delay between tries, and throws the last error if every attempt fails.
π‘ Hint
Loop from 1 to maxAttempts. Inside a try, return await operation(). In the catch, remember the error; if it wasn't the last attempt, await a small delay and continue. After the loop, throw the saved error.
β Solution
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
async function retry(operation, maxAttempts = 3) {
let lastError;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await operation(); // success β done
} catch (error) {
lastError = error;
console.warn(`Attempt ${attempt} failed: ${error.message}`);
if (attempt < maxAttempts) {
await delay(200 * attempt); // exponential-ish backoff
}
}
}
throw lastError; // all attempts exhausted
}
π― Quick Quiz
Question 1: When does a finally block run?
Question 2: Why should you throw new Error("...") instead of throw "..."?
Question 3: A plain synchronous try/catch wrapped around fetch(url).then(...) willβ¦
Best Practices & Pitfalls
β Do
- Catch the specific errors you can recover from; re-throw the rest
- Always log or handle a caught error β never leave the
catchblock empty - Throw
Errorobjects (or subclasses), never bare strings - Write descriptive messages:
`Cannot divide ${a} by zero`, not"Error" - Use
finallyfor cleanup that must run on every path - Check
response.okafter everyfetch
β Don't
- Swallow errors silently β
catch (e) {}hides real bugs - Wrap a giant block in one
tryso you can't tell which line failed - Use exceptions for ordinary control flow (return
nullfor "not found" instead) - Expect a synchronous
try/catchto catch a callback's error returnfrom afinallyblock β it overrides your real result
β οΈ The empty catch trap
// π« Looks harmless, hides everything
try { riskyOperation(); } catch (e) {}
// β
At minimum, log it
try {
riskyOperation();
} catch (error) {
console.error("riskyOperation failed:", error);
throw error; // or handle it β but never ignore it
}
Summary
π Key Takeaways
try/catchlets code recover from a thrown error instead of crashing- Every error has a
name,message, andstack; the built-in types (TypeError,ReferenceErrorβ¦) tell you the category finallyruns on every exit path β use it for cleanupthrow new Error()raises your own failures; custom Error subclasses let callers branch withinstanceof- Async errors need
awaitinsidetry/catchor a.catch()on the chain β and rememberfetchdoesn't reject on HTTP errors - Never swallow errors silently; catch specifically and re-throw the rest
π Additional Resources
π What's Next?
You can now handle errors gracefully β but how do you find the bugs that cause them in the first place? Next up: Debugging with Chrome DevTools, where you'll set breakpoints, inspect the call stack, and watch your code run one line at a time.
π Well handled!
Your programs no longer fall over at the first surprise. Robust software isn't error-free β it's error-ready.