Course: JavaScript Intermediate
Lesson 17: Error Handling
Level
Intermediate
Estimated Reading Time
10–15 minutes
Goal
By the end of this lesson, the learner will be able to:
- Use
try/catch/finallyto handle runtime errors gracefully - Access properties of the
Errorobject such asmessageandname - Throw custom errors with meaningful messages using
throw new Error()
Frontend Development Context
Frontend applications interact with user input, external APIs, and browser APIs — all of which can fail in unexpected ways. Without error handling, a single runtime error can crash the entire application or leave the user staring at a blank screen. Proper error handling means catching problems early, showing the user useful feedback, and keeping the application running.
Error handling also makes debugging faster. When you catch an error and log its message, you know exactly what went wrong and where. When you throw custom errors with descriptive messages, the developer (or future you) gets a clear explanation instead of a cryptic stack trace.
Explanation
A try/catch block lets you run code that might throw an error and handle the error gracefully if it does. Code inside try runs normally. If any line throws an error, execution jumps immediately to the catch block, skipping the rest of the try block. The catch block receives an Error object with details about what happened.
The Error object has two key properties: message (a human-readable description of the error) and name (the type of error, such as "TypeError", "ReferenceError", or "Error"). You can also access the stack property for a call stack trace, though its format varies across browsers.
The finally block runs after the try/catch completes, regardless of whether an error occurred. This is the right place for cleanup code: hiding loading spinners, closing connections, or resetting UI state that should always be restored.
You can throw your own errors using throw new Error("message"). This creates a standard Error object with a custom message. You can also throw custom error types by extending the built-in Error class, giving you more specific error categories.
Error handling should be targeted — wrap only the code that can actually fail, not your entire application. Catching too broadly can hide bugs. Always log errors or surface them to the user in some way.
Code Example
This example validates a user input, throws a custom error if invalid, and handles errors from a simulated async operation.
// Custom validation function that throws descriptive errors
function validateAge(age) {
if (typeof age !== "number") {
throw new TypeError(`Expected a number, got ${typeof age}`);
}
if (age < 0 || age > 120) {
throw new RangeError(`Age ${age} is out of valid range (0–120)`);
}
return true;
}
// Using try/catch to handle the thrown error
function processAge(input) {
try {
validateAge(input);
console.log(`Age ${input} is valid`);
} catch (error) {
console.error(`${error.name}: ${error.message}`);
} finally {
console.log("Validation attempt complete");
}
}
processAge(25); // "Age 25 is valid" | "Validation attempt complete"
processAge("25"); // "TypeError: Expected a number, got string" | "Validation attempt complete"
processAge(200); // "RangeError: Age 200 is out of valid range (0–120)" | "..."
// Handling JSON parse errors
function parseConfig(jsonString) {
try {
const config = JSON.parse(jsonString);
return config;
} catch (error) {
console.error("Failed to parse config:", error.message);
return {}; // return safe default
}
}
console.log(parseConfig('{"theme":"dark"}')); // { theme: "dark" }
console.log(parseConfig("not json")); // {} — error caught, default returned
Code Explanation
validateAge uses throw new TypeError(...) and throw new RangeError(...) to throw specific error types. Using the appropriate built-in error type (TypeError for wrong type, RangeError for out-of-bounds values) makes error messages more informative and allows callers to catch specific error types if needed.
processAge wraps validateAge in a try/catch. If validateAge throws, execution jumps to catch(error). The error object's name property is the error type and message is the description we set. Logging error.name + ": " + error.message gives a clear, formatted error report.
finally runs after either the try block succeeds or the catch block handles an error. It always runs — here it logs "Validation attempt complete" in all three cases.
parseConfig shows error handling for a common real-world task: parsing JSON from an unknown source. If the string is invalid JSON, JSON.parse throws a SyntaxError. The catch block returns an empty object as a safe fallback, preventing the error from crashing the calling code.
Pattern Highlights
✅ Positive Pattern: Throw errors with descriptive messages that explain what went wrong and what was expected.
throw new Error("userId is required")is far more helpful than a silent failure or a generic"Error"message.
⚠️ Neutral Note:
finallyalways runs — even if thetryblock has areturnstatement. Use this for cleanup that must always happen, but be aware that areturninfinallyoverrides thereturnintry.
❌ Negative Pattern: Do not use an empty
catchblock that silently swallows errors.catch (e) {}hides bugs and makes them nearly impossible to diagnose. Always at minimum log the error.
Common Mistakes
Mistake 1: Using an empty catch block
Catching errors but doing nothing with them.
try {
JSON.parse(badInput);
} catch (e) {
// silent — the error disappears
}
This hides the error completely. At minimum, log it: console.error(e.message). In user-facing code, also update the UI to show that something went wrong.
Mistake 2: Wrapping too much code in a single try block
Catching errors from many operations in one block, making it hard to know which line failed.
try {
const data = JSON.parse(raw);
const user = data.users[0];
renderProfile(user);
updateDashboard(user);
} catch (e) {
console.error("Something failed:", e.message); // which line?
}
Use separate try/catch blocks for independent operations that can fail in different ways, or check each result before proceeding to narrow down where errors can come from.
Mistake 3: Throwing a non-Error value
Using throw "error string" instead of throw new Error("message").
throw "Invalid ID"; // not an Error object
Throwing a string is valid JavaScript, but the catch block receives a string, not an Error object. You lose access to error.message, error.name, and error.stack. Always throw new Error(...) or a class that extends Error.
Quick Recap
try/catch/finallywraps code that might fail —catchhandles the error,finallyalways runs- The Error object has
name(error type),message(description), andstack(trace) properties - Use
throw new Error("message")to raise custom errors with descriptive messages - Never use empty
catchblocks — always log or surface the error in some meaningful way