JavaScript Error Handling
Error handling is a crucial part of programming that ensures programs can gracefully handle exceptions instead of crashing. JavaScript provides various error handling mechanisms, including try...catch statements, error objects, and custom errors. Mastering error handling is essential for writing robust, reliable JavaScript applications. In this chapter, we will learn about error handling in JavaScript.
What is Error Handling
Error handling refers to the mechanism that allows programs to catch, handle, and recover from exceptions during execution. Good error handling can:
- Prevent program crashes: Avoid stopping the entire application due to unhandled errors
- Provide user-friendly error messages: Help users understand what went wrong
- Facilitate debugging and maintenance: Provide detailed error information to help developers locate issues
- Improve program robustness: Enable programs to handle various exceptional situations
Error Types in JavaScript
JavaScript has several built-in error types:
1. Error (Basic Error Type)
const basicError = new Error("This is a basic error");
console.log(basicError.name); // "Error"
console.log(basicError.message); // "This is a basic error"
console.log(basicError.stack); // Error stack trace2. SyntaxError
try {
eval("const a = ;"); // Syntax error
} catch (error) {
console.log(error.name); // "SyntaxError"
console.log(error.message); // "Unexpected token ';'"
}3. ReferenceError
try {
console.log(undefinedVariable); // Reference to undeclared variable
} catch (error) {
console.log(error.name); // "ReferenceError"
console.log(error.message); // "undefinedVariable is not defined"
}4. TypeError
try {
const num = 123;
num.toUpperCase(); // Numbers don't have toUpperCase method
} catch (error) {
console.log(error.name); // "TypeError"
console.log(error.message); // "num.toUpperCase is not a function"
}5. RangeError
try {
const arr = new Array(-1); // Array length cannot be negative
} catch (error) {
console.log(error.name); // "RangeError"
console.log(error.message); // "Invalid array length"
}6. URIError
try {
decodeURIComponent("%"); // Invalid URI component
} catch (error) {
console.log(error.name); // "URIError"
console.log(error.message); // "URI malformed"
}try...catch Statement
The try...catch statement is the most basic error handling mechanism in JavaScript.
Basic Syntax
try {
// Code that might throw an error
console.log("Code being attempted");
} catch (error) {
// Error handling code
console.log("Caught error: " + error.message);
}Complete try...catch...finally Structure
try {
console.log("Code being attempted");
throw new Error("Manually thrown error");
} catch (error) {
console.log("Caught error: " + error.message);
} finally {
console.log("This code always executes regardless of errors");
}Error Object Properties
try {
throw new Error("Test error");
} catch (error) {
console.log("Error name: " + error.name); // "Error"
console.log("Error message: " + error.message); // "Test error"
console.log("Error stack: " + error.stack); // Location information
console.log("Is Error instance: " + (error instanceof Error)); // true
}throw Statement
The throw statement is used to manually throw errors.
Throwing Built-in Errors
function validateAge(age) {
if (age < 0) {
throw new RangeError("Age cannot be negative");
}
if (age > 150) {
throw new RangeError("Age cannot exceed 150");
}
return true;
}
try {
validateAge(-5);
} catch (error) {
console.log(error.name + ": " + error.message);
}Throwing Custom Errors
function divide(a, b) {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
try {
const result = divide(10, 0);
} catch (error) {
console.log("Calculation error: " + error.message);
}Custom Error Types
Creating custom error types can provide more specific error information.
Extending the Error Class
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
class NetworkError extends Error {
constructor(message, statusCode) {
super(message);
this.name = "NetworkError";
this.statusCode = statusCode;
}
}
// Use custom errors
function validateEmail(email) {
if (!email.includes("@")) {
throw new ValidationError("Invalid email format");
}
return true;
}
function fetchData(url) {
// Simulate network request failure
throw new NetworkError("Network request failed", 500);
}
// Handle different error types
try {
validateEmail("invalid-email");
} catch (error) {
if (error instanceof ValidationError) {
console.log("Validation error: " + error.message);
} else if (error instanceof NetworkError) {
console.log("Network error: " + error.message + " (Status: " + error.statusCode + ")");
} else {
console.log("Unknown error: " + error.message);
}
}Custom Errors with Additional Properties
class ApiError extends Error {
constructor(message, code, details = null) {
super(message);
this.name = "ApiError";
this.code = code;
this.details = details;
this.timestamp = new Date().toISOString();
}
}
// Use custom error with additional properties
try {
throw new ApiError("API call failed", "API_001", {
endpoint: "/api/users",
method: "GET",
userId: 123
});
} catch (error) {
if (error instanceof ApiError) {
console.log("API Error:");
console.log("Message: " + error.message);
console.log("Code: " + error.code);
console.log("Details: " + JSON.stringify(error.details));
console.log("Time: " + error.timestamp);
}
}Asynchronous Error Handling
Error handling in asynchronous operations requires special attention.
Promise Error Handling
// Use .catch() to handle Promise errors
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error("Data fetch failed"));
}, 1000);
});
}
fetchData()
.then(data => {
console.log("Data: " + data);
})
.catch(error => {
console.log("Promise error: " + error.message);
});
// Chained Promise error handling
function processUserData() {
return fetchData()
.then(data => {
return processData(data);
})
.then(processedData => {
return saveData(processedData);
})
.catch(error => {
console.log("Error in chain: " + error.message);
return { error: error.message, data: null };
});
}async/await Error Handling
// Use try...catch to handle async/await errors
async function handleUserData() {
try {
const data = await fetchData();
const processedData = await processData(data);
const result = await saveData(processedData);
return result;
} catch (error) {
console.log("Async operation error: " + error.message);
throw new Error("User data processing failed: " + error.message);
}
}
// Call async function
handleUserData()
.then(result => {
console.log("Success: " + result);
})
.catch(error => {
console.log("Final error: " + error.message);
});Parallel Async Operations Error Handling
// Promise.all error handling
async function fetchMultipleData() {
try {
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
return { users, posts, comments };
} catch (error) {
console.log("Error in parallel operation: " + error.message);
throw error;
}
}
// Promise.allSettled handling (ES2020)
async function fetchMultipleDataGracefully() {
const results = await Promise.allSettled([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
const successful = results
.filter(result => result.status === "fulfilled")
.map(result => result.value);
const errors = results
.filter(result => result.status === "rejected")
.map(result => result.reason);
console.log("Successful results: " + successful.length);
console.log("Number of errors: " + errors.length);
return { successful, errors };
}Global Error Handling
window.onerror (Browser Environment)
// Global JavaScript error handling
window.onerror = function(message, source, lineno, colno, error) {
console.log("Global error caught:");
console.log("Message: " + message);
console.log("Source file: " + source);
console.log("Line number: " + lineno);
console.log("Column number: " + colno);
console.log("Error object: " + error);
// Can send error info to server for logging
// sendErrorToServer({ message, source, lineno, colno, error });
// Return true to prevent default error handling
return true;
};unhandledrejection (Unhandled Promise Rejection)
// Handle unhandled Promise rejections
window.addEventListener("unhandledrejection", function(event) {
console.log("Unhandled Promise rejection:");
console.log("Reason: " + event.reason);
console.log("Promise: " + event.promise);
// Prevent default handling (showing error in console)
event.preventDefault();
});Node.js Environment Global Error Handling
// Global error handling in Node.js
process.on("uncaughtException", (error) => {
console.log("Uncaught exception: " + error.message);
console.log(error.stack);
// Log error and exit gracefully
// logError(error);
process.exit(1);
});
process.on("unhandledRejection", (reason, promise) => {
console.log("Unhandled Promise rejection: " + reason);
console.log("Promise: " + promise);
// Can choose to exit process or continue running
// process.exit(1);
});Error Handling Best Practices
1. Early Validation and Error Checking
function calculateBMI(weight, height) {
// Parameter validation
if (typeof weight !== "number" || weight <= 0) {
throw new ValidationError("Weight must be a positive number");
}
if (typeof height !== "number" || height <= 0) {
throw new ValidationError("Height must be a positive number");
}
if (height > 3) {
throw new ValidationError("Height unit should be meters");
}
// Calculate BMI
const bmi = weight / (height * height);
return Math.round(bmi * 100) / 100;
}
// Usage example
try {
const bmi = calculateBMI(70, 1.75);
console.log("BMI: " + bmi);
} catch (error) {
console.log("Calculation error: " + error.message);
}2. Provide Meaningful Error Messages
class UserService {
static users = [];
static addUser(userData) {
// Validate required fields
if (!userData.name) {
throw new ValidationError("User name is required");
}
if (!userData.email) {
throw new ValidationError("User email is required");
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(userData.email)) {
throw new ValidationError("Invalid email format");
}
// Check if email already exists
if (this.users.some(user => user.email === userData.email)) {
throw new ValidationError("Email already in use");
}
// Create user
const user = {
id: Date.now(),
...userData,
createdAt: new Date()
};
this.users.push(user);
return user;
}
static getUserById(id) {
const user = this.users.find(user => user.id === id);
if (!user) {
throw new Error(`User with ID ${id} not found`);
}
return user;
}
}3. Error Logging
class ErrorLogger {
static log(error, context = {}) {
const errorInfo = {
timestamp: new Date().toISOString(),
message: error.message,
name: error.name,
stack: error.stack,
context: context,
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "Node.js",
url: typeof window !== "undefined" ? window.location.href : "Node.js"
};
// Output to console in development environment
if (process.env.NODE_ENV === "development") {
console.error("Error log:", errorInfo);
}
// Send to server for logging
// this.sendToServer(errorInfo);
return errorInfo;
}
static sendToServer(errorInfo) {
// Simulate sending to server
fetch("/api/errors", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(errorInfo)
}).catch(sendError => {
console.error("Failed to send error log:", sendError);
});
}
}4. Graceful Error Recovery
class DataFetcher {
static async fetchWithRetry(url, maxRetries = 3) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
lastError = error;
console.log(`Attempt ${i + 1} failed: ${error.message}`);
// If not last attempt, wait before retrying
if (i < maxRetries - 1) {
await this.delay(1000 * (i + 1)); // Incremental delay
}
}
}
// All retries failed
throw new Error(`Failed after ${maxRetries} retries: ${lastError.message}`);
}
static delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Use fetch with retry
DataFetcher.fetchWithRetry("https://api.example.com/data")
.then(data => {
console.log("Data fetched successfully:", data);
})
.catch(error => {
console.log("Final failure:", error.message);
});5. User-Friendly Error Display
class ErrorHandler {
static displayUserFriendlyError(error) {
let userMessage = "An unknown error occurred. Please try again later";
if (error instanceof ValidationError) {
userMessage = error.message;
} else if (error instanceof NetworkError) {
userMessage = "Network connection failed. Please check your network settings";
} else if (error.message.includes("timeout")) {
userMessage = "Request timed out. Please try again later";
} else if (error.message.includes("404")) {
userMessage = "The requested resource was not found";
} else if (error.message.includes("500")) {
userMessage = "Server internal error. Please try again later";
}
// Display error message to user
this.showMessage(userMessage, "error");
// Log detailed error information
console.error("Detailed error info:", error);
}
static showMessage(message, type = "info") {
// Create error message element
const messageElement = document.createElement("div");
messageElement.className = `message message-${type}`;
messageElement.textContent = message;
// Add to page
document.body.appendChild(messageElement);
// Auto remove after 3 seconds
setTimeout(() => {
if (messageElement.parentNode) {
messageElement.parentNode.removeChild(messageElement);
}
}, 3000);
}
}Practical Example: Form Validation
class FormValidator {
constructor(formElement) {
this.form = formElement;
this.errors = new Map();
this.init();
}
init() {
this.form.addEventListener("submit", (event) => {
event.preventDefault();
this.validateAndSubmit();
});
// Real-time validation
this.form.querySelectorAll("input, select, textarea").forEach(field => {
field.addEventListener("blur", () => {
this.validateField(field);
});
});
}
async validateAndSubmit() {
try {
// Clear previous errors
this.clearErrors();
// Validate all fields
const isValid = this.validateAllFields();
if (!isValid) {
throw new ValidationError("Please fix errors in the form");
}
// Get form data
const formData = new FormData(this.form);
const data = Object.fromEntries(formData);
// Submit data
this.showLoading(true);
const result = await this.submitData(data);
this.showSuccess("Submitted successfully!");
return result;
} catch (error) {
this.handleError(error);
} finally {
this.showLoading(false);
}
}
validateField(field) {
const fieldName = field.name;
const value = field.value.trim();
let errorMessage = "";
// Validate based on field type
switch (fieldName) {
case "email":
if (!value) {
errorMessage = "Email is required";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
errorMessage = "Please enter a valid email address";
}
break;
case "password":
if (!value) {
errorMessage = "Password is required";
} else if (value.length < 8) {
errorMessage = "Password must be at least 8 characters";
}
break;
default:
if (field.hasAttribute("required") && !value) {
errorMessage = "This field is required";
}
}
this.setFieldError(field, errorMessage);
return !errorMessage;
}
validateAllFields() {
let isValid = true;
this.form.querySelectorAll("input, select, textarea").forEach(field => {
if (!this.validateField(field)) {
isValid = false;
}
});
return isValid;
}
setFieldError(field, errorMessage) {
const fieldName = field.name;
if (errorMessage) {
this.errors.set(fieldName, errorMessage);
field.classList.add("error");
} else {
this.errors.delete(fieldName);
field.classList.remove("error");
}
}
handleError(error) {
if (error instanceof ValidationError) {
this.showMessage(error.message, "error");
} else {
this.showMessage("Submission failed. Please try again later", "error");
console.error("Form submission error:", error);
}
// Scroll to first error field
const firstErrorField = this.form.querySelector(".error");
if (firstErrorField) {
firstErrorField.scrollIntoView({ behavior: "smooth", block: "center" });
firstErrorField.focus();
}
}
clearErrors() {
this.errors.clear();
this.form.querySelectorAll(".error").forEach(field => {
field.classList.remove("error");
});
}
showLoading(show) {
const submitButton = this.form.querySelector("button[type='submit']");
if (submitButton) {
submitButton.disabled = show;
submitButton.textContent = show ? "Submitting..." : "Submit";
}
}
showSuccess(message) {
this.showMessage(message, "success");
}
showMessage(message, type) {
// Remove previous message
const existingMessage = this.form.querySelector(".form-message");
if (existingMessage) {
existingMessage.remove();
}
// Create new message
const messageElement = document.createElement("div");
messageElement.className = `form-message message-${type}`;
messageElement.textContent = message;
// Add to top of form
this.form.insertBefore(messageElement, this.form.firstChild);
// Auto remove after 3 seconds
setTimeout(() => {
if (messageElement.parentNode) {
messageElement.remove();
}
}, 3000);
}
async submitData(data) {
const response = await fetch("/api/submit", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data)
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `Submission failed: ${response.status}`);
}
return await response.json();
}
}
// Usage example
// const validator = new FormValidator(document.getElementById("myForm"));Summary
Key points about JavaScript error handling:
- Error Types: Error, SyntaxError, ReferenceError, TypeError, RangeError, URIError, EvalError
- Basic Syntax: try...catch...finally statement
- Error Throwing: throw statement and custom error types
- Async Error Handling: Promise.catch(), async/await + try...catch
- Global Error Handling: window.onerror, unhandledrejection event
- Custom Errors: Extend Error class for specific business errors
- Best Practices: Early validation, meaningful error messages, error logging, graceful recovery, user-friendly display
Mastering error handling is a key skill for writing high-quality JavaScript applications. In the next chapter, we will learn about JavaScript debugging techniques.