Skip to main content

Error Handling Strategies in JavaScript: Try-Catch, Promises, and Async/Await

Master JavaScript error handling with try-catch, Promises, and async/await. Learn best practices, common pitfalls, and practical patterns for robust applications.

Table of Contents

Introduction

Error handling is one of the most critical aspects of writing robust JavaScript applications. Whether you’re building a simple website or a complex enterprise application, how you handle errors can make the difference between a frustrating user experience and a polished, professional product. Yet, error handling in JavaScript can be particularly challenging due to its asynchronous nature, multiple execution contexts, and various error types.

JavaScript provides several mechanisms for handling errors: traditional try-catch blocks for synchronous code, .catch() methods for Promises, and try-catch with async/await for modern asynchronous code. Each approach has its strengths, use cases, and potential pitfalls. Understanding when and how to use each method is essential for writing maintainable, reliable code.

This comprehensive guide will teach you everything you need to know about error handling in JavaScript. You’ll learn how to handle errors in synchronous and asynchronous contexts, understand different error types, discover advanced patterns for complex scenarios, and avoid common mistakes that can lead to silent failures or poor user experiences. By the end, you’ll have the knowledge and patterns needed to build resilient JavaScript applications that gracefully handle errors and provide meaningful feedback to users.


Understanding JavaScript Errors

Before diving into error handling strategies, it’s essential to understand the different types of errors in JavaScript and how they’re structured.

Error Types in JavaScript

JavaScript has several built-in error types, each representing different categories of problems:

// SyntaxError: Invalid syntax
const invalid = function() {; // Missing closing brace
// ReferenceError: Variable doesn't exist
console.log(nonExistentVariable);
// TypeError: Wrong type of value
const num = null;
num.toUpperCase(); // Cannot read property 'toUpperCase' of null
// RangeError: Value out of range
const arr = new Array(-1); // Invalid array length
// URIError: Invalid URI handling
decodeURIComponent('%'); // Malformed URI sequence
// EvalError: Error in eval() function (rarely used)

The Error Object

All errors in JavaScript are instances of the Error object or one of its subclasses. The Error object has several useful properties:

try {
throw new Error("Something went wrong");
} catch (error) {
console.log(error.name); // "Error"
console.log(error.message); // "Something went wrong"
console.log(error.stack); // Stack trace (in most environments)
}

Creating Custom Errors

You can create custom error classes for better error categorization and handling:

class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = "ValidationError";
this.field = field;
}
}
class NetworkError extends Error {
constructor(message, statusCode) {
super(message);
this.name = "NetworkError";
this.statusCode = statusCode;
}
}
// Usage
function validateEmail(email) {
if (!email.includes("@")) {
throw new ValidationError("Invalid email format", "email");
}
}
try {
validateEmail("invalid-email");
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Validation failed for ${error.field}: ${error.message}`);
}
}

Error Propagation

In JavaScript, errors propagate up the call stack until they’re caught or reach the global scope:

function level1() {
level2();
}
function level2() {
level3();
}
function level3() {
throw new Error("Error from level 3");
}
try {
level1();
} catch (error) {
console.log("Caught at top level:", error.message);
// Error propagates from level3 → level2 → level1 → catch block
}

Synchronous Error Handling with Try-Catch

The try-catch-finally statement is JavaScript’s primary mechanism for handling errors in synchronous code.

Basic Try-Catch Syntax

try {
// Code that might throw an error
const result = riskyOperation();
console.log("Success:", result);
} catch (error) {
// Handle the error
console.error("Error occurred:", error.message);
} finally {
// Always executes, regardless of success or failure
console.log("Cleanup code here");
}

Multiple Catch Blocks

While JavaScript doesn’t support multiple catch blocks like some languages, you can use conditional logic to handle different error types:

try {
// Some operation
processData(data);
} catch (error) {
if (error instanceof ValidationError) {
// Handle validation errors
showValidationError(error.field, error.message);
} else if (error instanceof NetworkError) {
// Handle network errors
showNetworkError(error.statusCode);
} else {
// Handle unexpected errors
logError(error);
showGenericError();
}
}

Nested Try-Catch Blocks

You can nest try-catch blocks for granular error handling:

try {
const user = getUserData();
try {
const profile = user.profile;
const avatar = profile.avatar.url; // Might throw if profile is null
} catch (error) {
// Handle missing profile gracefully
console.log("Using default avatar");
user.profile = { avatar: { url: "/default-avatar.png" } };
}
displayUser(user);
} catch (error) {
// Handle user data fetch errors
console.error("Failed to load user:", error.message);
}

Try-Catch Limitations with Asynchronous Code

⚠️ Important: Try-catch blocks only catch errors from synchronous code. They cannot catch errors from asynchronous operations:

// ❌ This won't work as expected
try {
setTimeout(() => {
throw new Error("Async error");
}, 1000);
} catch (error) {
// This will never execute!
console.log("Caught:", error);
}
// The error will be unhandled because it occurs after the try-catch has already completed

For asynchronous code, you need different error handling strategies, which we’ll cover next.


Error Handling with Promises

Promises provide a built-in mechanism for handling errors through the .catch() method and the second parameter of .then().

Using .catch() Method

The .catch() method is the most common way to handle errors in Promises:

fetch("/api/users")
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then((data) => {
console.log("Users:", data);
})
.catch((error) => {
console.error("Error fetching users:", error.message);
// Handle the error appropriately
});

Chaining Multiple Promises

When chaining multiple Promises, a single .catch() can handle errors from any point in the chain:

fetch("/api/users")
.then((response) => response.json())
.then((users) => {
return fetch(`/api/users/${users[0].id}/posts`);
})
.then((response) => response.json())
.then((posts) => {
console.log("Posts:", posts);
})
.catch((error) => {
// Catches errors from any step in the chain
console.error("Error in promise chain:", error.message);
});

Returning Errors vs Throwing

You can return error objects instead of throwing, but this requires explicit checking:

// ❌ Not recommended - requires manual error checking
fetch("/api/users")
.then((response) => {
if (!response.ok) {
return { error: new Error("Failed to fetch") };
}
return response.json();
})
.then((data) => {
if (data.error) {
// Manual error handling
console.error(data.error);
return;
}
console.log("Users:", data);
});
// ✅ Better - use throw to trigger catch
fetch("/api/users")
.then((response) => {
if (!response.ok) {
throw new Error("Failed to fetch");
}
return response.json();
})
.then((data) => {
console.log("Users:", data);
})
.catch((error) => {
console.error("Error:", error.message);
});

Promise.all() Error Handling

Promise.all() fails fast - if any promise rejects, the entire operation fails:

// All promises must succeed
Promise.all([fetch("/api/users"), fetch("/api/posts"), fetch("/api/comments")])
.then((responses) => Promise.all(responses.map((r) => r.json())))
.then(([users, posts, comments]) => {
console.log("All data loaded:", { users, posts, comments });
})
.catch((error) => {
// If any request fails, this catches it
console.error("One or more requests failed:", error.message);
});

Promise.allSettled() for Partial Failures

Use Promise.allSettled() when you want to handle partial failures:

Promise.allSettled([
fetch("/api/users"),
fetch("/api/posts"),
fetch("/api/comments"),
]).then((results) => {
results.forEach((result, index) => {
if (result.status === "fulfilled") {
console.log(`Request ${index} succeeded:`, result.value);
} else {
console.error(`Request ${index} failed:`, result.reason);
}
});
});

Promise.race() Error Handling

Promise.race() resolves or rejects with the first promise that settles:

// Race between API call and timeout
const timeout = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Request timeout")), 5000);
});
Promise.race([fetch("/api/slow-endpoint"), timeout])
.then((response) => response.json())
.then((data) => {
console.log("Data received:", data);
})
.catch((error) => {
if (error.message === "Request timeout") {
console.error("Request took too long");
} else {
console.error("Request failed:", error.message);
}
});

Converting Callbacks to Promises

When working with callback-based APIs, wrap them in Promises for better error handling:

function readFilePromise(filePath) {
return new Promise((resolve, reject) => {
fs.readFile(filePath, "utf8", (error, data) => {
if (error) {
reject(error); // Convert callback error to Promise rejection
} else {
resolve(data);
}
});
});
}
// Now you can use .catch() for error handling
readFilePromise("data.json")
.then((data) => JSON.parse(data))
.then((json) => console.log("Parsed:", json))
.catch((error) => {
console.error("Error reading file:", error.message);
});

Error Handling with Async/Await

Async/await provides a more synchronous-looking syntax for handling asynchronous code, making error handling more intuitive.

Basic Try-Catch with Async/Await

Async/await allows you to use try-catch blocks with asynchronous code:

async function fetchUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error("Error fetching user:", error.message);
throw error; // Re-throw to let caller handle it
}
}

Handling Multiple Async Operations

You can handle errors from multiple async operations in a single try-catch:

async function loadUserDashboard(userId) {
try {
const [user, posts, comments] = await Promise.all([
fetch(`/api/users/${userId}`).then((r) => r.json()),
fetch(`/api/users/${userId}/posts`).then((r) => r.json()),
fetch(`/api/users/${userId}/comments`).then((r) => r.json()),
]);
return { user, posts, comments };
} catch (error) {
console.error("Failed to load dashboard:", error.message);
throw error;
}
}

Error Handling in Async Loops

When using async/await in loops, handle errors carefully:

// ✅ Sequential processing with error handling
async function processUsers(userIds) {
const results = [];
for (const userId of userIds) {
try {
const user = await fetchUserData(userId);
results.push({ userId, user, success: true });
} catch (error) {
results.push({ userId, error: error.message, success: false });
// Continue processing other users
}
}
return results;
}
// ✅ Parallel processing with individual error handling
async function processUsersParallel(userIds) {
const promises = userIds.map(async (userId) => {
try {
const user = await fetchUserData(userId);
return { userId, user, success: true };
} catch (error) {
return { userId, error: error.message, success: false };
}
});
return Promise.all(promises);
}

Async Function Error Propagation

Async functions always return Promises, so errors can be caught with .catch():

async function asyncOperation() {
throw new Error("Something went wrong");
}
// Both approaches work:
asyncOperation().catch((error) => console.error("Caught:", error.message));
// Or:
try {
await asyncOperation();
} catch (error) {
console.error("Caught:", error.message);
}

Handling Errors in Async Arrow Functions

const fetchData = async (url) => {
try {
const response = await fetch(url);
return await response.json();
} catch (error) {
console.error("Fetch error:", error.message);
return null; // Return default value instead of throwing
}
};
// Usage
const data = await fetchData("/api/data");
if (data) {
console.log("Data loaded:", data);
}

Advanced Error Handling Patterns

For complex applications, you’ll need more sophisticated error handling patterns.

Error Boundaries Pattern

Create a centralized error handling system:

class ErrorHandler {
static handle(error, context = {}) {
// Log error
console.error("Error:", error.message, context);
// Send to error tracking service (e.g., Sentry)
if (window.errorTracker) {
window.errorTracker.captureException(error, { extra: context });
}
// Show user-friendly message
this.showUserMessage(error);
// Return standardized error response
return {
success: false,
error: {
message: this.getUserFriendlyMessage(error),
code: error.code || "UNKNOWN_ERROR",
timestamp: new Date().toISOString(),
},
};
}
static getUserFriendlyMessage(error) {
if (error instanceof NetworkError) {
return "Unable to connect to the server. Please check your internet connection.";
}
if (error instanceof ValidationError) {
return `Invalid input: ${error.message}`;
}
return "An unexpected error occurred. Please try again.";
}
static showUserMessage(error) {
// Show toast notification or modal
const message = this.getUserFriendlyMessage(error);
// Implementation depends on your UI framework
}
}
// Usage
try {
await riskyOperation();
} catch (error) {
ErrorHandler.handle(error, { operation: "riskyOperation", userId: 123 });
}

Retry Pattern with Exponential Backoff

Implement automatic retry logic for transient failures:

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (!response.ok && response.status >= 500) {
// Retry on server errors
throw new Error(`Server error: ${response.status}`);
}
return await response.json();
} catch (error) {
lastError = error;
if (attempt < maxRetries - 1) {
// Exponential backoff: wait 2^attempt seconds
const delay = Math.pow(2, attempt) * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
console.log(`Retry attempt ${attempt + 1} after ${delay}ms`);
}
}
}
throw lastError;
}
// Usage
try {
const data = await fetchWithRetry("/api/data");
console.log("Data:", data);
} catch (error) {
console.error("Failed after retries:", error.message);
}

Circuit Breaker Pattern

Prevent cascading failures with a circuit breaker:

class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.failureCount = 0;
this.threshold = threshold;
this.timeout = timeout;
this.state = "CLOSED"; // CLOSED, OPEN, HALF_OPEN
this.nextAttempt = Date.now();
}
async execute(fn) {
if (this.state === "OPEN") {
if (Date.now() < this.nextAttempt) {
throw new Error("Circuit breaker is OPEN");
}
this.state = "HALF_OPEN";
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = "CLOSED";
}
onFailure() {
this.failureCount++;
if (this.failureCount >= this.threshold) {
this.state = "OPEN";
this.nextAttempt = Date.now() + this.timeout;
}
}
}
// Usage
const breaker = new CircuitBreaker(5, 60000);
async function callAPI() {
return breaker.execute(async () => {
const response = await fetch("/api/data");
if (!response.ok) throw new Error("API failed");
return response.json();
});
}

Error Wrapper Utility

Create a utility to wrap async functions with consistent error handling:

function withErrorHandling(fn, errorHandler) {
return async (...args) => {
try {
return await fn(...args);
} catch (error) {
if (errorHandler) {
return errorHandler(error, ...args);
}
throw error;
}
};
}
// Usage
const safeFetchUser = withErrorHandling(
async (userId) => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
},
(error, userId) => {
console.error(`Failed to fetch user ${userId}:`, error.message);
return { id: userId, error: "User not found" };
},
);
// Now safeFetchUser never throws - always returns a value
const user = await safeFetchUser(123);

Global Error Handlers

Set up global error handlers for unhandled errors:

// Handle unhandled promise rejections
window.addEventListener("unhandledrejection", (event) => {
console.error("Unhandled promise rejection:", event.reason);
// Prevent default browser error logging
event.preventDefault();
// Send to error tracking service
if (window.errorTracker) {
window.errorTracker.captureException(event.reason);
}
// Show user-friendly error message
showErrorNotification("Something went wrong. Please try again.");
});
// Handle general errors
window.addEventListener("error", (event) => {
console.error("Global error:", event.error);
// Send to error tracking service
if (window.errorTracker) {
window.errorTracker.captureException(event.error);
}
// Prevent default browser error display
event.preventDefault();
});

Error Handling Best Practices

Follow these best practices to write robust, maintainable error handling code.

✅ Always Handle Errors Explicitly

// ✅ Good - explicit error handling
async function fetchData() {
try {
const response = await fetch("/api/data");
return await response.json();
} catch (error) {
console.error("Failed to fetch data:", error);
return null; // Return safe default
}
}
// ❌ Bad - errors are silently ignored
async function fetchData() {
const response = await fetch("/api/data");
return await response.json(); // Unhandled errors!
}

✅ Provide Context in Error Messages

// ✅ Good - includes context
function validateUser(user) {
if (!user.email) {
throw new ValidationError("Email is required", "email", user);
}
if (!user.email.includes("@")) {
throw new ValidationError("Invalid email format", "email", user);
}
}
// ❌ Bad - generic error message
function validateUser(user) {
if (!user.email) {
throw new Error("Invalid"); // What's invalid? Which field?
}
}

✅ Use Specific Error Types

// ✅ Good - specific error types
try {
await fetchUser(userId);
} catch (error) {
if (error instanceof NetworkError) {
showNetworkErrorMessage();
} else if (error instanceof NotFoundError) {
showNotFoundMessage();
} else {
showGenericErrorMessage();
}
}
// ❌ Bad - generic error handling
try {
await fetchUser(userId);
} catch (error) {
showGenericErrorMessage(); // Doesn't differentiate error types
}

✅ Don’t Swallow Errors

// ❌ Bad - error is swallowed
try {
await riskyOperation();
} catch (error) {
// Error is ignored - bad!
}
// ✅ Good - error is logged or re-thrown
try {
await riskyOperation();
} catch (error) {
console.error("Operation failed:", error);
// Either handle it properly or re-throw
throw error;
}

✅ Clean Up Resources in Finally Blocks

// ✅ Good - cleanup in finally
let connection;
try {
connection = await openDatabaseConnection();
await connection.query("SELECT * FROM users");
} catch (error) {
console.error("Query failed:", error);
} finally {
if (connection) {
await connection.close(); // Always cleanup
}
}

✅ Handle Errors at the Right Level

// ✅ Good - handle errors at appropriate level
async function fetchUserPosts(userId) {
try {
const user = await fetchUser(userId);
const posts = await fetchPosts(userId);
return { user, posts };
} catch (error) {
// Handle at component/service level
if (error instanceof NotFoundError) {
return { user: null, posts: [] };
}
throw error; // Re-throw unexpected errors
}
}
// ❌ Bad - error handling too deep in call stack
async function fetchUserPosts(userId) {
const user = await fetchUser(userId).catch(() => null);
const posts = await fetchPosts(userId).catch(() => []);
// Lost error context and can't differentiate error types
}

Common Pitfalls and Anti-Patterns

Avoid these common mistakes when handling errors in JavaScript.

❌ Catching Errors Too Broadly

// ❌ Bad - catches everything, even programming errors
try {
const result = someOperation();
} catch (error) {
// This catches ALL errors, including ReferenceError, TypeError, etc.
console.log("Something went wrong");
}
// ✅ Good - catch specific errors or re-throw unexpected ones
try {
const result = someOperation();
} catch (error) {
if (error instanceof ExpectedError) {
handleExpectedError(error);
} else {
throw error; // Re-throw unexpected errors
}
}

❌ Ignoring Promise Rejections

// ❌ Bad - unhandled promise rejection
async function doSomething() {
await fetch("/api/data"); // If this fails, error is unhandled
}
// ✅ Good - handle promise rejections
async function doSomething() {
try {
await fetch("/api/data");
} catch (error) {
console.error("Failed:", error);
}
}

❌ Error Messages Exposed to Users

// ❌ Bad - technical error messages exposed
try {
await saveUserData(data);
} catch (error) {
alert(`Error: ${error.message}`); // "Cannot read property 'id' of undefined"
}
// ✅ Good - user-friendly messages
try {
await saveUserData(data);
} catch (error) {
const message =
error instanceof ValidationError
? "Please check your input and try again."
: "Unable to save data. Please try again later.";
alert(message);
}

❌ Nested Try-Catch Hell

// ❌ Bad - deeply nested try-catch blocks
async function complexOperation() {
try {
const user = await fetchUser();
try {
const posts = await fetchPosts(user.id);
try {
const comments = await fetchComments(posts[0].id);
// ... more nesting
} catch (error) {
// Handle comments error
}
} catch (error) {
// Handle posts error
}
} catch (error) {
// Handle user error
}
}
// ✅ Good - flat error handling
async function complexOperation() {
try {
const user = await fetchUser();
const posts = await fetchPosts(user.id);
const comments = await fetchComments(posts[0].id);
return { user, posts, comments };
} catch (error) {
// Handle all errors in one place
if (error instanceof UserNotFoundError) {
return handleUserNotFound();
}
if (error instanceof PostsError) {
return handlePostsError();
}
throw error;
}
}

❌ Using Try-Catch for Control Flow

// ❌ Bad - using exceptions for control flow
function findUser(users, id) {
try {
return (
users.find((u) => u.id === id) ||
(() => {
throw new Error();
})()
);
} catch (error) {
return null;
}
}
// ✅ Good - use proper control flow
function findUser(users, id) {
return users.find((u) => u.id === id) || null;
}

Real-World Error Handling Examples

Let’s look at practical examples of error handling in common scenarios.

Example 1: API Request with Retry Logic

class ApiClient {
constructor(baseURL, options = {}) {
this.baseURL = baseURL;
this.maxRetries = options.maxRetries || 3;
this.retryDelay = options.retryDelay || 1000;
}
async request(endpoint, options = {}) {
let lastError;
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
try {
const url = `${this.baseURL}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
// Don't retry client errors (4xx)
if (response.status >= 400 && response.status < 500) {
throw new Error(`Client error: ${response.status}`);
}
// Retry server errors (5xx)
throw new Error(`Server error: ${response.status}`);
}
return await response.json();
} catch (error) {
lastError = error;
// Don't retry on client errors
if (error.message.includes("Client error")) {
throw error;
}
// Wait before retrying
if (attempt < this.maxRetries - 1) {
await this.delay(this.retryDelay * (attempt + 1));
}
}
}
throw lastError;
}
delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
// Usage
const api = new ApiClient("https://api.example.com");
try {
const users = await api.request("/users");
console.log("Users:", users);
} catch (error) {
console.error("Failed to fetch users:", error.message);
}

Example 2: Form Validation with Error Handling

class FormValidator {
static async validate(formData) {
const errors = [];
// Validate email
if (!formData.email) {
errors.push(new ValidationError("Email is required", "email"));
} else if (!this.isValidEmail(formData.email)) {
errors.push(new ValidationError("Invalid email format", "email"));
}
// Validate password
if (!formData.password) {
errors.push(new ValidationError("Password is required", "password"));
} else if (formData.password.length < 8) {
errors.push(
new ValidationError(
"Password must be at least 8 characters",
"password",
),
);
}
// Check if email exists (async validation)
if (formData.email && this.isValidEmail(formData.email)) {
try {
const exists = await this.checkEmailExists(formData.email);
if (exists) {
errors.push(new ValidationError("Email already registered", "email"));
}
} catch (error) {
// Network error - don't block form submission
console.warn("Could not verify email:", error.message);
}
}
if (errors.length > 0) {
throw new ValidationError("Form validation failed", errors);
}
return true;
}
static isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
static async checkEmailExists(email) {
const response = await fetch(`/api/check-email?email=${email}`);
const data = await response.json();
return data.exists;
}
}
// Usage in form submission
async function handleSubmit(event) {
event.preventDefault();
const formData = {
email: event.target.email.value,
password: event.target.password.value,
};
try {
await FormValidator.validate(formData);
await submitForm(formData);
showSuccessMessage("Account created successfully!");
} catch (error) {
if (error instanceof ValidationError) {
showValidationErrors(error.errors);
} else {
showErrorMessage("Failed to create account. Please try again.");
}
}
}

Example 3: Error Handling in React Components

import { useState, useEffect } from "react";
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUser() {
try {
setLoading(true);
setError(null);
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
if (response.status === 404) {
throw new NotFoundError("User not found");
}
throw new Error(`Failed to fetch user: ${response.status}`);
}
const userData = await response.json();
setUser(userData);
} catch (error) {
setError(error);
// Log error for debugging
console.error("Error fetching user:", error);
// Send to error tracking service
if (window.errorTracker) {
window.errorTracker.captureException(error);
}
} finally {
setLoading(false);
}
}
fetchUser();
}, [userId]);
if (loading) {
return <div>Loading...</div>;
}
if (error) {
if (error instanceof NotFoundError) {
return <div>User not found</div>;
}
return <div>Error loading user profile. Please try again.</div>;
}
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}

Example 4: Error Handling in Node.js Express Route

const express = require("express");
const router = express.Router();
// Error handling middleware
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// Route with error handling
router.get(
"/users/:id",
asyncHandler(async (req, res) => {
const { id } = req.params;
// Validate input
if (!id || isNaN(id)) {
throw new ValidationError("Invalid user ID");
}
// Database query
const user = await db.users.findById(id);
if (!user) {
throw new NotFoundError("User not found");
}
res.json(user);
}),
);
// Global error handler middleware
router.use((error, req, res, next) => {
console.error("Error:", error);
if (error instanceof ValidationError) {
return res.status(400).json({
error: {
message: error.message,
code: "VALIDATION_ERROR",
},
});
}
if (error instanceof NotFoundError) {
return res.status(404).json({
error: {
message: error.message,
code: "NOT_FOUND",
},
});
}
// Generic error
res.status(500).json({
error: {
message: "Internal server error",
code: "INTERNAL_ERROR",
},
});
});

Conclusion

Error handling is a fundamental skill for building robust JavaScript applications. Throughout this guide, we’ve explored the various mechanisms JavaScript provides for handling errors: try-catch for synchronous code, .catch() for Promises, and try-catch with async/await for modern asynchronous code.

Key takeaways from this guide:

  • Understand error types: Know the difference between different error types and when to use custom error classes
  • Choose the right tool: Use try-catch for synchronous code, Promises for callback-based async code, and async/await for modern async code
  • Handle errors explicitly: Never ignore errors or let them propagate unhandled
  • Provide context: Include meaningful error messages and context to help with debugging
  • Use patterns: Implement retry logic, circuit breakers, and error boundaries for complex applications
  • Avoid anti-patterns: Don’t use exceptions for control flow, don’t swallow errors, and don’t expose technical details to users

Remember that good error handling isn’t just about catching errors—it’s about providing a great user experience even when things go wrong. By following the patterns and best practices outlined in this guide, you’ll be able to build applications that gracefully handle errors and provide meaningful feedback to users.

For more advanced topics, consider exploring related concepts like JavaScript Promises and Async/Await, Understanding the JavaScript Event Loop, and Testing Strategies for Modern Web Applications to further strengthen your JavaScript skills.

The JavaScript error handling ecosystem continues to evolve, with new patterns and tools emerging regularly. Stay updated with the latest best practices and always prioritize user experience when designing your error handling strategies.