Error Handling
Reviewed & published by Brayan K
Errors are not signs of failure — they are signals. Learn everything from basic try/catch to advanced real-world error architectures used in production apps.
Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- Catch errors with try/catch/finally
- Throw your own custom errors
- Identify built-in error types (TypeError, ReferenceError)
- Handle errors in async code
- Create custom error classes
- Build robust production-level error systems
💡 Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:
- Download Node.js to run JavaScript on your computer
- Use your browser's Developer Console (Press F12) to test code snippets
- Create a .html file with <script> tags and open it in your browser
Why Error Handling Matters
When errors are ignored:
- Your app crashes suddenly
- Users lose progress
- Data becomes corrupted
- Security becomes weaker
- Debugging becomes 10× harder
JavaScript gives you extremely powerful tools to handle, detect, filter, categorize, and recover from errors — synchronously AND asynchronously.
1. What Is an Error in JavaScript?
An error occurs when JavaScript cannot complete an operation. Examples:
- Trying to use an undefined variable
- Calling a function that doesn't exist
- Invalid API responses
- Network failures
- Logic errors in your code
- Unexpected data formats
By default, errors stop execution. Your job is to catch, interpret, and recover from them.
2. Try...Catch — The Foundation
try {
// Code that might fail
riskyOperation();
} catch (error) {
console.error("Error occurred:", error.message);
} finally {
console.log("Cleanup runs no matter what.");
}- try runs your potentially-dangerous code
- catch receives the error if thrown
- finally ALWAYS runs (cleanup, closing files, removing loaders, etc.)
3. Throwing Your Own Errors
JavaScript allows you to create and throw your own errors:
function divide(a, b) {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
try {
divide(10, 0);
} catch (error) {
console.log("Caught:", error.message);
}Why throw custom errors?
- You can prevent further execution
- You can communicate clearly what went wrong
- You can enforce data validation
- You create predictable behaviour
4. Built-In Error Types
Incorrect data type. Example:
null.toUpperCase();console.log(x); // x is not declaredlet a = ; // invalidValue outside allowed range:
new Array(-2);Triggered by malformed URI sequences
5. Using Error Names to Handle Smartly
try {
risky();
} catch (error) {
if (error instanceof TypeError) {
console.log("Type problem!");
} else if (error instanceof ReferenceError) {
console.log("You referenced something wrong!");
} else {
console.log("Unknown error:", error.message);
}
}This lets you create fine-grained error logic.
Worked Example — Which Lines Actually Run
Everything above described the machinery. This runs it. The single most useful thing to understand about try/catch is which lines execute and in what order, so every line in this program is numbered. Run it, then read the output next to the code and trace the jumps.
Three things to watch for: nothing after a throw in the try block ever runs; finally runs on every path, error or not; and an error thrown inside a function is caught by whoever called it.
// WORKED EXAMPLE - watch exactly which lines run, and in what order.
// Every console.log below is numbered so you can follow the path the engine
// takes when something goes wrong.
console.log("=== A: a try block that does NOT throw ===");
try {
console.log("1. inside try");
console.log("2. still inside try");
} catch (error) {
console.log("3. this line never runs - nothing threw");
} finally {
console.log("4. finally always runs");
}
console.log("\n=== B: a try block that DOES throw ===");
try {
console.log("1. inside try");
throw new Error("the network is down"); // execution jumps to catch RIGHT HERE
console.log("2. UNREACHABLE - anything after a throw is skipped");
} catch (error) {
// The catch parameter is an Error object, not a string.
console.log("3. caught:", error.message); // the text you passed to new Error
console.log("4. its name:", error.name); // "Error" for a plain Error
} finally {
console.log("5. finally runs after the catch, too");
}
console.log("\n=== C: errors travel up to whoever called you ===");
// withdraw does not handle its own errors. It throws, and the caller decides.
function withdraw(balance, amount) {
if (typeof amount !== "number") {
throw new TypeError("amount must be a number");
}
if (amount > balance) {
throw new RangeError("insufficient funds");
}
return balance - amount;
}
// One catch block, three different outcomes, chosen by the TYPE of the error.
function tryWithdraw(balance, amount) {
try {
const left = withdraw(balance, amount);
console.log("ok, £" + left + " left");
} catch (error) {
if (error instanceof TypeError) {
console.log("bad input: " + error.message);
} else if (error instanceof RangeError) {
console.log("declined: " + error.message);
} else {
throw error; // not ours to handle - pass it upwards untouched
}
} finally {
console.log(" (transaction closed)"); // runs on every path above
}
}
tryWithdraw(100, 30);
tryWithdraw(100, "thirty");
tryWithdraw(100, 300);
// ✅ Expected output:
// === A: a try block that does NOT throw ===
// 1. inside try
// 2. still inside try
// 4. finally always runs
//
// === B: a try block that DOES throw ===
// 1. inside try
// 3. caught: the network is down
// 4. its name: Error
// 5. finally runs after the catch, too
//
// === C: errors travel up to whoever called you ===
// ok, £70 left
// (transaction closed)
// bad input: amount must be a number
// (transaction closed)
// declined: insufficient funds
// (transaction closed)
//
// Notice what is MISSING from section B: line 2 never printed. That single
// gap is why you put the throw last, or accept that the rest is dead code.🎯 Your Turn — A Seat Booking Check
Your turn to write the four keywords this lesson is built on. The logic is already there; the blanks are throw, the error type to use, and the two block keywords that go with try. Pick the error type on meaning: one problem is a wrong kind of value, the other is a number that is out of range.
// 🎯 YOUR TURN - fill in the four blanks marked ___
// A tiny seat-booking check.
function bookSeat(seatsLeft, wanted) {
if (typeof wanted !== "number") {
___ new TypeError("wanted must be a number"); // 👉 replace ___ with throw
}
if (wanted > seatsLeft) {
throw new ___("not enough seats"); // 👉 a number out of range: RangeError
}
return seatsLeft - wanted;
}
function attempt(seatsLeft, wanted) {
try {
const left = bookSeat(seatsLeft, wanted);
console.log("booked, " + left + " seats left");
} ___ (error) { // 👉 the block that receives the error
console.log(error.name + ": " + error.message);
} ___ { // 👉 the block that runs either way
console.log("--- request handled ---");
}
}
attempt(10, 4);
attempt(10, "four");
attempt(10, 40);
// ✅ Expected output once the blanks are filled:
// booked, 6 seats left
// --- request handled ---
// TypeError: wanted must be a number
// --- request handled ---
// RangeError: not enough seats
// --- request handled ---
//
// If "--- request handled ---" appears only once, the last blank is wrong:
// that line has to be in the block that runs on every path, not inside try.6. Error Handling in Asynchronous Code
async function getUser() {
try {
const res = await fetch(url);
if (!res.ok) throw new Error("HTTP Error " + res.status);
return await res.json();
} catch (err) {
console.error("Fetch failed:", err.message);
return { error: true, msg: err.message };
}
}Why async error handling matters:
- Real-world JavaScript is 70% async
- Network issues happen
- Users have slow internet
- Data can be malformed
7. Re-Throwing Errors
Sometimes you want to catch an error, log it, then pass it upward:
async function loadData() {
try {
return await getData();
} catch (err) {
console.error("Handled locally:", err.message);
throw err; // re-throw
}
}- Logging errors
- Adding context
- Creating layered architecture
8. Creating Custom Error Classes
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
function validateAge(age) {
if (age < 0) throw new ValidationError("Age cannot be negative");
}Benefits of custom errors:
- Cleaner error organization
- More predictable logic
- Easier debugging
- Perfect for large apps
9. Nested Try...Catch Blocks
try {
try {
throw new Error("Inner failure");
} catch (inner) {
console.log("Inner caught:", inner.message);
throw new Error("Outer failure");
}
} catch (outer) {
console.log("Outer caught:", outer.message);
}Nested errors often appear when:
- Parsing inside processing
- Handling multiple API calls
- Running multi-stage operations
11. Real-World Example — Payment Processing
function chargeCard(card, amount) {
if (!card.number) {
throw new ValidationError("Card number missing");
}
if (amount <= 0) {
throw new ValidationError("Amount must be greater than 0");
}
// Simulate charge...
return { success: true };
}
// With layered handling:
try {
chargeCard(card, 10);
} catch (err) {
if (err instanceof ValidationError) {
console.log("User made a mistake:", err.message);
} else {
console.log("Internal failure:", err.message);
}
}This mimics real e-commerce systems.
12. Real-World Example — Robust API Fetching
Now your UI doesn't crash when the API fails — it gracefully recovers.
13. Real-World Example — Form Validation
function validateForm(user) {
if (!user.email.includes("@")) {
throw new ValidationError("Invalid email format");
}
if (user.password.length < 8) {
throw new ValidationError("Password too short");
}
return true;
}
// Frontend:
try {
validateForm(user);
console.log("Form valid");
} catch (err) {
showErrorToUser(err.message);
}14. Advanced Async Error Handling
Asynchronous code is where 90% of real production errors happen.
- APIs timeout
- Backend services go down
- Internet drops
- JSON parsing fails
- Unexpected response shape
- Race conditions
- Authentication tokens expire
You MUST master async error strategies to build real-world websites, apps, and SaaS systems.
15. Fetch Errors Aren't Always "Errors"
fetch() only throws errors for:
- Network down
- Domain unreachable
- CORS failures
- Request blocked
⚠️ It does NOT throw errors for bad HTTP codes (404, 500, 403, 429)
if (!res.ok) {
throw new Error("HTTP " + res.status);
}Without this check, your code will behave like everything is fine when it absolutely isn't.
16. Retry Logic with Exponential Backoff
Real-world apps must recover from errors gracefully:
async function retryFetch(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const res = await fetch(url);
if (res.ok) return res.json();
} catch (err) {
if (i === retries - 1) throw err;
}
await new Promise(r => setTimeout(r, 200 * (i + 1)));
}
}Retries at: 200ms → 400ms → 600ms. This is used by Google, AWS, Netflix, Stripe, PayPal.
17. Defensive Programming
Defensive programming = writing code that assumes things will go wrong.
function safeJSON(response) {
try {
return response.json();
} catch {
return { error: "Invalid JSON" };
}
}function safeMultiply(a, b) {
if (typeof a !== "number" || typeof b !== "number") {
throw new TypeError("Arguments must be numbers");
}
return a * b;
}const city = user?.address?.location?.city ?? "Unknown";18. Global Error Handling
Most frameworks implement a global error handler:
Browser Global Error Handler:
window.onerror = function(message, source, line, column, error) {
console.log("Global error:", message);
};window.onunhandledrejection = function(event) {
console.log("Unhandled rejection:", event.reason);
};This prevents silent failures.
19. Production-Grade Error Handler
A complete production-grade Fetch handler:
- Cancellation
- Status errors
- Network errors
- JSON parsing
🎯 Mini-Challenge — A Config Parser That Fails Helpfully
"Invalid config" is a useless error message. "ConfigError on line 2: missing =" gets the problem fixed in ten seconds. The difference is a custom error class carrying one extra field, and that is what you are going to build.
No starter logic below — only the brief and the expected output. Everything you need has appeared earlier in this lesson: class ... extends Error, throw, instanceof and finally.
// 🎯 MINI-CHALLENGE: a config parser that fails helpfully
//
// 1. Create class ConfigError extends Error taking (message, line).
// In the constructor: call super(message), set this.name = "ConfigError"
// and store this.line = line.
//
// 2. Write parseConfig(text):
// - split text on "\n" and walk the lines with their index
// - trim each line; skip completely blank ones
// - a line with no "=" is invalid: throw a ConfigError("missing =", N)
// where N is the 1-BASED line number (so index + 1)
// - otherwise split at the FIRST "=" (line.indexOf("=") helps) and put
// the key and value into an object
// - return that object
//
// 3. Loop over the two inputs below. Inside a try/catch/finally:
// - on success print Object.keys(config).join(", ")
// - on a ConfigError print "ConfigError on line " + err.line + ": " + err.message
// - re-throw anything that is NOT a ConfigError
// - in finally, print "done"
const good = "host=localhost\nport=8080\n\ndebug=true";
const bad = "host=localhost\nport 8080";
// your code here
// ✅ Expected output:
// host, port, debug
// done
// ConfigError on line 2: missing =
// done
//
// Watch the line number: the blank third line of "good" must be skipped
// without being counted as an error, and the bad file reports line 2, not
// line 1 and not line 3.20. Common JavaScript Error Scenarios
- ✔ JSON parsing failure
- ✔ Network offline
- ✔ Missing properties
- ✔ Undefined variable
- ✔ Null access
- ✔ Wrong type
- ✔ Not catching async error
- ✔ Silent Promise rejection
- ✔ Invalid function argument
- ✔ Race condition
You will encounter these daily in real development.
🎯 Practice Projects for Mastery
Project 1 — "Safe Calculator"
- Validate numbers
- Throw errors for wrong input
- Catch and display friendly messages
- Try/catch around each operation
Project 2 — "Robust Fetch Wrapper"
Build a function: safeFetch(url, retries = 3)
- Throw on bad HTTP codes
- Distinguish network vs HTTP error
- Return JSON or fallback
Project 3 — "Form Validator"
- Use multiple custom error classes
- Validate email, password, username
- Show UI messages
Project 4 — "Global Error Handler Demo"
- Demonstrate window.onerror
- Demonstrate window.onunhandledrejection
📋 Quick Reference — Error Handling
| Method | Purpose |
|---|---|
| try...catch | Catch errors in a block of code |
| throw | Manually trigger an error |
| finally | Run code regardless of outcome |
| Error Object | Has .message and .stack |
| Custom Error | class MyError extends Error |
Lesson Complete — Error Handling!
You've mastered the art of writing robust code that handles failure gracefully. This separates amateur code from professional software.
Practice quiz
In a try/catch/finally block, when does the finally block run?
- Only when an error is thrown
- Only when no error is thrown
- Always, regardless of the outcome
- Never, it is optional
Answer: Always, regardless of the outcome. finally always runs, making it ideal for cleanup like closing connections or removing loaders.
Which keyword do you use to manually trigger an error?
- throw
- raise
- error
- catch
Answer: throw. throw manually triggers an error, for example throw new Error('Cannot divide by zero').
Which error type is thrown by null.toUpperCase()?
- ReferenceError
- SyntaxError
- RangeError
- TypeError
Answer: TypeError. Calling a method on null is an incorrect data type operation, which throws a TypeError.
Which error type occurs when you use a variable that was never declared?
- TypeError
- ReferenceError
- RangeError
- URIError
Answer: ReferenceError. Referencing an undeclared variable throws a ReferenceError.
How do you create a custom error class?
- class MyError extends Error { }
- function MyError()
- new CustomError()
- Error.create('My')
Answer: class MyError extends Error { }. Custom errors extend the built-in Error class, e.g. class ValidationError extends Error.
Does fetch() throw an error for HTTP status codes like 404 or 500?
- Yes, always
- Only for 500
- No, you must check res.ok yourself
- Only in strict mode
Answer: No, you must check res.ok yourself. fetch only rejects for network-level failures; you must check res.ok for bad HTTP status codes.
What does 're-throwing' an error mean?
- Catching an error and ignoring it
- Catching an error, handling it locally, then throwing it again to pass it upward
- Throwing two errors at once
- Converting an error to a string
Answer: Catching an error, handling it locally, then throwing it again to pass it upward. Re-throwing lets you log or add context locally, then pass the error up with throw err.
How can you handle errors in async/await code?
- With a .then() only
- Errors cannot be caught in async code
- With window.onerror only
- By wrapping the awaited calls in try/catch
Answer: By wrapping the awaited calls in try/catch. async/await uses ordinary try/catch around the awaited operations to catch failures.
In an outer/inner nested try/catch, where is an error thrown in the inner try caught first?
- The outer catch
- The inner catch
- Both simultaneously
- Neither
Answer: The inner catch. The nearest enclosing catch handles it first; the inner catch catches the inner error.
Which is a recommended error-handling best practice from the lesson?
- Show database stack traces to users
- Ignore async errors
- Use specific error messages and don't leak sensitive info
- Fail silently
Answer: Use specific error messages and don't leak sensitive info. Use clear, specific messages, handle async errors, and never leak sensitive details to users.
Continue this course
- Previous: Fetch API
- Next: Closures and Scope — Understand how functions remember variables from their outer scope
- Quick reference: JavaScript cheat sheet
- From the blog: Async/Await in JavaScript Explained