JavaScript Error Handling
Errors are inevitable in any program. A user might enter invalid data, a network request might fail, or a function might be called with unexpected arguments. Proper error handling allows programs to respond to these situations gracefully instead of crashing.
Types of JavaScript Errors
| Error Type | Cause | Example |
|---|---|---|
| SyntaxError | Invalid code syntax — caught before running | Missing bracket or quote |
| ReferenceError | Using a variable that doesn't exist | console.log(x) where x is not declared |
| TypeError | Wrong data type used in an operation | null.toUpperCase() |
| RangeError | Value is out of allowed range | new Array(-1) |
| URIError | Malformed URI used | decodeURI('%') |
try...catch — The Core of Error Handling
The try...catch statement lets code attempt a potentially risky operation and catch any errors that occur, preventing the program from crashing.
Syntax:
try {
// Code that might cause an error
} catch (error) {
// Code that runs if an error occurs
}try {
let result = undefined.toUpperCase(); // TypeError!
} catch (error) {
console.log("Error caught:", error.message);
// Output: Cannot read properties of undefined (reading 'toUpperCase')
}Without try...catch, this error would stop the entire script. With it, execution continues after the catch block.
The Error Object
The error object inside catch contains useful information about what went wrong.
try {
let data = JSON.parse("{invalid json}");
} catch (error) {
console.log("Name:", error.name); // SyntaxError
console.log("Message:", error.message); // Unexpected token i in JSON...
console.log("Stack:", error.stack); // Full error trace
}The finally Block
The finally block always runs — whether an error occurred or not. It is used for cleanup tasks like closing a connection or hiding a loading spinner.
function loadData() {
try {
console.log("Loading data...");
// Simulating an error
throw new Error("Network failure!");
} catch (error) {
console.log("Failed:", error.message);
} finally {
console.log("Cleanup: hiding loader."); // Always runs
}
}
loadData();
// Loading data...
// Failed: Network failure!
// Cleanup: hiding loader.Throwing Custom Errors
The throw statement creates and throws a custom error manually. This is useful for enforcing business rules and input validation.
function divide(a, b) {
if (b === 0) {
throw new Error("Division by zero is not allowed.");
}
return a / b;
}
try {
let result = divide(10, 0);
console.log(result);
} catch (error) {
console.log("Error:", error.message);
}
// Error: Division by zero is not allowed.Throwing Different Error Types
function setAge(age) {
if (typeof age !== "number") {
throw new TypeError("Age must be a number.");
}
if (age < 0 || age > 150) {
throw new RangeError("Age must be between 0 and 150.");
}
return age;
}
try {
setAge("twenty");
} catch (error) {
if (error instanceof TypeError) {
console.log("Type issue:", error.message);
} else if (error instanceof RangeError) {
console.log("Range issue:", error.message);
}
}
// Type issue: Age must be a number.Custom Error Classes
In larger applications, custom error classes allow creating specific error types that can be caught and handled separately.
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
function validateUser(user) {
if (!user.name) {
throw new ValidationError("Name is required.", "name");
}
if (!user.email || !user.email.includes("@")) {
throw new ValidationError("Invalid email address.", "email");
}
return true;
}
try {
validateUser({ name: "", email: "notanemail" });
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Field "${error.field}" has error: ${error.message}`);
}
}
// Field "name" has error: Name is required.Error Handling in Asynchronous Code
With Promises
fetch("https://api.example.com/data")
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.log("Fetch failed:", error.message))
.finally(() => console.log("Request complete."));With async/await
async function getData() {
try {
let response = await fetch("https://api.example.com/data");
let data = await response.json();
console.log(data);
} catch (error) {
console.log("Failed to fetch:", error.message);
} finally {
console.log("Done.");
}
}
getData();Defensive Programming with Validation
Combining error handling with input validation prevents most runtime errors from occurring in the first place.
function calculateDiscount(price, percent) {
if (typeof price !== "number" || typeof percent !== "number") {
throw new TypeError("Both price and percent must be numbers.");
}
if (price < 0) {
throw new RangeError("Price cannot be negative.");
}
if (percent < 0 || percent > 100) {
throw new RangeError("Discount percent must be between 0 and 100.");
}
return price - (price * percent / 100);
}
try {
let finalPrice = calculateDiscount(1000, 20);
console.log("Discounted Price:", finalPrice); // 800
} catch (error) {
console.log(error.name + ":", error.message);
}try...catch...finally Structure
| Block | Purpose | When It Runs |
|---|---|---|
| try | Code that might cause an error | Always, first |
| catch | Handle the error if one occurs | Only when an error is thrown |
| finally | Cleanup code | Always, last (error or not) |
Key Points to Remember
- Use
try...catchto handle errors gracefully without crashing the program - The
errorobject in the catch block hasname,message, andstackproperties - The
finallyblock always runs — use it for cleanup tasks - Use
throwto create and raise custom errors manually - Use
instanceofto check the type of a caught error and handle different errors differently - Create custom error classes by extending the built-in
Errorclass - In async/await code, wrap
awaitcalls in try/catch to catch promise rejections
