Table of Contents
- What Are Errors in JavaScript?
- 1.1 The
ErrorObject - 1.2 Common Error Types
- 1.1 The
- Basic Error Handling with
try/catch/finally- 2.1 How
try/catchWorks - 2.2 The
finallyBlock
- 2.1 How
- Throwing Custom Errors with
throw- 3.1 Creating Custom Error Classes
- Handling Asynchronous Errors
- 4.1 Errors in Promises
- 4.2 Errors with
async/await
- Unhandled Errors: Catching the Uncatchable
- 5.1
window.onerror(Browser) - 5.2
processEvents (Node.js)
- 5.1
- Best Practices for Graceful Error Handling
- 6.1 Be Specific with Error Types
- 6.2 Provide Contextual Information
- 6.3 Avoid Silent Failures
- 6.4 Clean Up Resources with
finally - 6.5 Validate Inputs Early
- 6.6 Log Errors Strategically
- Advanced: Error Boundaries (React)
- Conclusion
- References
What Are Errors in JavaScript?
Errors in JavaScript are objects that represent unexpected conditions or failures during code execution. They disrupt the normal flow of a program and, if unhandled, can cause scripts to crash.
1.1 The Error Object
All built-in errors in JavaScript inherit from the Error object, which has two primary properties:
message: A human-readable description of the error.name: The type of error (e.g.,SyntaxError,ReferenceError).
Additional properties like stack (a stack trace for debugging) are also available in most environments (browsers and Node.js).
1.2 Common Error Types
JavaScript defines several built-in error types to categorize different failure scenarios:
| Error Type | Description | Example |
|---|---|---|
SyntaxError | Invalid JavaScript syntax (e.g., missing brackets). | console.log('Hello' (missing ))) |
ReferenceError | Accessing an undefined variable or non-existent property. | console.log(undefinedVariable) |
TypeError | Operation performed on a value of the wrong type. | 'string'.push('a') (strings have no push method) |
RangeError | Value is outside the allowable range (e.g., invalid array length). | new Array(-1) |
URIError | Invalid URI encoding/decoding (e.g., decodeURI('%')). | decodeURIComponent('%z') |
EvalError | Error in eval() (rarely used today). | eval('invalid code') |
Basic Error Handling with try/catch/finally
The try/catch/finally statement is JavaScript’s primary mechanism for handling synchronous errors. It lets you “try” to execute risky code, “catch” errors if they occur, and “finally” run cleanup code regardless of success or failure.
2.1 How try/catch Works
try {
// Code that might throw an error
const riskyOperation = JSON.parse('{ invalid json }'); // Throws SyntaxError
console.log('This line runs only if no error occurs');
} catch (error) {
// Code to handle the error
console.error('Caught an error:', error.message); // "Unexpected token i in JSON at position 2"
console.error('Error type:', error.name); // "SyntaxError"
}
tryBlock: Contains code that may throw an error. If an error occurs here, execution immediately jumps to thecatchblock.catchBlock: Takes anerrorparameter (the thrown error object) and handles it (e.g., logging, user feedback).
2.2 The finally Block
The finally block runs always, whether an error occurred or not. Use it for cleanup tasks like closing files, aborting network requests, or resetting state.
let connection;
try {
connection = openDatabaseConnection(); // Hypothetical function
connection.query('SELECT * FROM users');
} catch (error) {
console.error('Database error:', error.message);
} finally {
// Always close the connection, even if an error occurred
if (connection) connection.close();
console.log('Connection closed');
}
Throwing Custom Errors with throw
While JavaScript throws built-in errors, you can throw custom errors to make your code more expressive. Use throw with an Error object (or a subclass) to signal specific failure conditions.
3.1 Creating Custom Error Classes
For better error categorization, extend the native Error class to create domain-specific errors (e.g., ValidationError, NetworkError). This preserves the Error prototype chain, ensuring compatibility with instanceof checks.
class ValidationError extends Error {
constructor(message) {
super(message); // Call parent Error constructor
this.name = 'ValidationError'; // Override the error name
}
}
// Usage
function validateUser(user) {
if (!user.email) {
throw new ValidationError('Email is required'); // Throw custom error
}
if (!user.age || user.age < 18) {
throw new ValidationError('Age must be at least 18');
}
}
try {
validateUser({ name: 'Alice' }); // Missing email → throws ValidationError
} catch (error) {
if (error instanceof ValidationError) {
console.error('Validation failed:', error.message); // "Email is required"
} else {
console.error('Unexpected error:', error.message);
}
}
Why custom errors? They let you distinguish between error types (e.g., a ValidationError vs. a NetworkError) and handle them differently.
Handling Asynchronous Errors
Asynchronous code (e.g., promises, async/await, callbacks) requires special error-handling techniques, as try/catch alone won’t catch errors in async operations.
4.1 Errors in Promises
Promises use the .catch() method to handle errors. If a promise rejects, the nearest .catch() in the chain catches it.
// Fetch data from an API (returns a promise)
fetch('https://api.example.com/data')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`); // Throw custom error for non-2xx status
}
return response.json();
})
.then(data => console.log('Data:', data))
.catch(error => {
console.error('Fetch failed:', error.message); // Handles network errors or HTTP errors
});
4.2 Errors with async/await
async/await syntax simplifies promise handling, and you can use try/catch with it to handle async errors synchronously.
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Async error caught:', error.message);
// Re-throw if you want the caller to handle it
throw error; // Optional: Propagate the error to the caller
}
}
// Usage
fetchData().catch(error => console.error('Caller caught:', error.message));
Key Note: Always handle errors in async functions—unhandled promise rejections crash Node.js apps and log warnings in browsers.
Unhandled Errors: Catching the Uncatchable
Even with try/catch, some errors might slip through (e.g., unhandled promise rejections, errors in event listeners). Use global handlers to catch these “last-resort” errors.
5.1 window.onerror (Browser)
In browsers, window.onerror catches uncaught synchronous errors and unhandled promise rejections (via window.onunhandledrejection).
// Catch synchronous errors
window.onerror = (message, source, lineno, colno, error) => {
console.error(`Global error: ${message} at ${source}:${lineno}:${colno}`);
return true; // Prevents the browser from logging the error to the console
};
// Catch unhandled promise rejections
window.onunhandledrejection = (event) => {
console.error('Unhandled promise rejection:', event.reason.message);
event.preventDefault(); // Prevents the browser from logging the warning
};
5.2 process Events (Node.js)
In Node.js, use process events to catch uncaught exceptions and unhandled promise rejections.
// Catch uncaught synchronous exceptions
process.on('uncaughtException', (error) => {
console.error('Uncaught Exception:', error.message);
// Gracefully shut down the app (e.g., close databases)
process.exit(1); // Required: Uncaught exceptions leave the app in an unstable state
});
// Catch unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'Reason:', reason.message);
// Optional: Exit or recover (Node.js 15+ terminates by default for unhandled rejections)
});
Best Practices for Graceful Error Handling
Follow these practices to ensure your error handling is robust and user-friendly:
6.1 Be Specific with Error Types
Avoid generic catch (error) blocks that handle all errors the same way. Use instanceof checks or error name properties to handle specific errors differently.
try {
riskyOperation();
} catch (error) {
if (error instanceof ValidationError) {
showUserMessage('Invalid input: ' + error.message); // User-friendly message
} else if (error instanceof NetworkError) {
showUserMessage('Network issue. Please try again later.');
} else {
logToService(error); // Log unexpected errors for debugging
showUserMessage('Something went wrong.');
}
}
6.2 Provide Contextual Information
Include details like timestamps, user IDs, or operation names in errors to simplify debugging.
class NetworkError extends Error {
constructor(message, url, statusCode) {
super(`${message} (URL: ${url}, Status: ${statusCode})`);
this.name = 'NetworkError';
this.url = url;
this.statusCode = statusCode;
}
}
// Usage
throw new NetworkError('Failed to fetch user', '/api/user/123', 404);
// Error message: "Failed to fetch user (URL: /api/user/123, Status: 404)"
6.3 Avoid Silent Failures
Never leave empty catch blocks—they hide bugs and make debugging impossible.
// BAD: Silent failure
try {
riskyOperation();
} catch (error) {
// Do nothing
}
// GOOD: Log or handle the error
try {
riskyOperation();
} catch (error) {
console.error('Silent failure avoided:', error);
// Or re-throw: throw error;
}
6.4 Clean Up Resources with finally
Always release resources (e.g., file handles, network connections, timers) in finally to prevent leaks.
function readFileWithCleanup() {
const file = openFile('data.txt'); // Hypothetical function
try {
return file.read();
} catch (error) {
console.error('Read error:', error);
} finally {
file.close(); // Always close the file
}
}
6.5 Validate Inputs Early
Prevent errors by validating inputs before executing risky operations. Use libraries like Joi or Zod for complex validation.
function calculateArea(radius) {
if (typeof radius !== 'number' || radius <= 0) {
throw new ValidationError('Radius must be a positive number');
}
return Math.PI * radius ** 2; // No error risk here!
}
6.6 Log Errors Strategically
Log errors to monitoring tools (e.g., Sentry, Datadog) in production, but avoid logging sensitive data (e.g., passwords, API keys).
function logError(error) {
const sanitizedError = {
message: error.message,
name: error.name,
stack: error.stack,
// Omit sensitive fields: userPassword, apiKey, etc.
};
fetch('/api/log-error', {
method: 'POST',
body: JSON.stringify(sanitizedError),
});
}
Advanced: Error Boundaries (React)
In React, error boundaries are components that catch errors in their child component tree, log them, and display fallback UI instead of crashing the entire app. They’re React’s way of handling component-level errors.
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state to trigger fallback UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Log error to an error reporting service
logErrorToService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// Fallback UI
return <h1>Something went wrong. Please refresh the page.</h1>;
}
return this.props.children;
}
}
// Usage: Wrap risky components
<ErrorBoundary>
<UserProfile userId={123} /> {/* If this crashes, ErrorBoundary shows fallback */}
</ErrorBoundary>
Note: Error boundaries do not catch errors in:
- Event handlers (use
try/catchhere) - Server-side rendering
- Errors thrown in the error boundary itself
Conclusion
Graceful error handling is a cornerstone of robust JavaScript applications. By using try/catch/finally, throwing custom errors, handling async operations carefully, and following best practices like validating inputs and logging strategically, you can build apps that remain stable, user-friendly, and easy to debug.
Remember: Errors are inevitable—what matters is how you handle them.