Error Handling in Node.js: Synchronous and Asynchronous Patterns
Complete guide to error handling in Node.js — try-catch, callbacks, promises, async/await, and uncaught exception handling
Introduction
Error handling is critical in Node.js applications. Unlike browser JavaScript, an unhandled error in Node.js can crash your entire server. Understanding both synchronous and asynchronous error handling patterns is essential for building reliable backend applications.
Synchronous Error Handling
Try-Catch
The standard pattern for synchronous code:
function parseJSON(jsonString) {
try {
const data = JSON.parse(jsonString);
return { success: true, data };
} catch (error) {
return { success: false, error: error.message };
}
}
const result = parseJSON('{"valid": true}');
console.log(result); // { success: true, data: { valid: true } }
const badResult = parseJSON('{invalid}');
console.log(result); // { success: false, error: 'Unexpected token...' }
Throwing Custom Errors
class ValidationError extends Error {
constructor(message, field) {
super(message);
this.name = 'ValidationError';
this.field = field;
}
}
class DatabaseError extends Error {
constructor(message, query) {
super(message);
this.name = 'DatabaseError';
this.query = query;
}
}
function validateUser(user) {
if (!user.email) {
throw new ValidationError('Email is required', 'email');
}
if (!user.name) {
throw new ValidationError('Name is required', 'name');
}
}
try {
validateUser({ email: 'test@example.com' });
} catch (error) {
if (error instanceof ValidationError) {
console.error(`Validation failed on ${error.field}: ${error.message}`);
} else {
throw error; // Re-throw unknown errors
}
}
Asynchronous Error Handling
The Callback Pattern (Legacy)
import fs from 'node:fs';
// Error-first callback pattern
fs.readFile('config.json', 'utf8', (error, data) => {
if (error) {
console.error('Failed to read config:', error.message);
return;
}
try {
const config = JSON.parse(data);
console.log('Config loaded:', config);
} catch (parseError) {
console.error('Invalid config format:', parseError.message);
}
});
// Always check the error parameter first!
Promises
import fs from 'node:fs/promises';
// Promise chain with catch
fs.readFile('data.json', 'utf8')
.then(data => JSON.parse(data))
.then(config => {
console.log('Config:', config);
})
.catch(error => {
if (error.code === 'ENOENT') {
console.error('File not found');
} else if (error instanceof SyntaxError) {
console.error('Invalid JSON');
} else {
console.error('Unexpected error:', error);
}
});
Async/Await (Modern, Preferred)
import fs from 'node:fs/promises';
async function loadConfig() {
try {
const data = await fs.readFile('config.json', 'utf8');
const config = JSON.parse(data);
return config;
} catch (error) {
if (error.code === 'ENOENT') {
console.error('Config file not found, using defaults');
return { port: 3000, host: 'localhost' };
}
if (error instanceof SyntaxError) {
console.error('Invalid config JSON');
throw new Error('Configuration is corrupted');
}
throw error; // Re-throw unexpected errors
}
}
// Usage
const config = await loadConfig();
Handling Multiple Async Operations
// Promise.all — fail if any reject
try {
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
} catch (error) {
console.error('Failed to load dashboard:', error);
}
// Promise.allSettled — handle each result individually
const results = await Promise.allSettled([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Operation ${index} succeeded`);
} else {
console.error(`Operation ${index} failed:`, result.reason);
}
});
Global Error Handling
Uncaught Exceptions
// Catches synchronous errors that escape try-catch
process.on('uncaughtException', (error) => {
console.error('Uncaught exception:', error);
// Perform cleanup, then exit
server.close(() => process.exit(1));
// Always exit — the process is in an unknown state
});
Unhandled Promise Rejections
// Catches promises that reject without a catch handler
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection:', reason);
// Log, alert, but don't necessarily crash
});
Express Error Middleware
import express from 'express';
const app = express();
// Route handler
app.get('/users/:id', async (req, res, next) => {
try {
const user = await fetchUser(req.params.id);
res.json(user);
} catch (error) {
next(error); // Pass to error middleware
}
});
// Error handling middleware (4 parameters)
app.use((error, req, res, next) => {
console.error('Error:', error.message);
if (error instanceof ValidationError) {
return res.status(400).json({ error: error.message, field: error.field });
}
if (error instanceof DatabaseError) {
return res.status(500).json({ error: 'Internal server error' });
}
res.status(500).json({ error: 'Something went wrong' });
});
Error Handling Patterns
Result Pattern
// Return result objects instead of throwing
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
return { success: false, error: `HTTP ${response.status}` };
}
const data = await response.json();
return { success: true, data };
} catch (error) {
return { success: false, error: error.message };
}
}
// Usage
const result = await fetchUser(1);
if (result.success) {
console.log(result.data);
} else {
console.error(result.error);
}
Retry Pattern
async function withRetry(fn, maxRetries = 3, delay = 1000) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries) throw error;
console.warn(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2; // Exponential backoff
}
}
}
// Usage
const data = await withRetry(() => fetch('https://api.example.com/data'));
Graceful Shutdown
async function shutdown(server) {
console.log('Shutting down gracefully...');
// Stop accepting new connections
server.close();
// Close database connections
await db.disconnect();
// Flush logs
await logger.flush();
console.log('Shutdown complete');
process.exit(0);
}
process.on('SIGTERM', () => shutdown(server));
process.on('SIGINT', () => shutdown(server));
Best Practices
Do's
// ✅ Always handle promise rejections
await operation().catch(handleError);
// ✅ Use custom error classes
throw new ValidationError('Invalid input', 'email');
// ✅ Log errors with context
console.error('User fetch failed', { userId: id, error: error.message });
// ✅ Handle specific errors differently
if (error.code === 'ENOENT') { /* file not found */ }
// ✅ Use async/await with try-catch
try { await operation(); } catch (error) { /* ... */ }
Don'ts
// ❌ Don't swallow errors silently
try { await operation(); } catch (error) {} // Empty catch!
// ❌ Don't throw strings
throw 'Something failed'; // Use Error objects
// ❌ Don't ignore the error in callbacks
fs.readFile('file.txt', (err, data) => {
console.log(data); // What if err?
});
// ❌ Don't use sync methods in async contexts
const data = fs.readFileSync('file.txt'); // Blocks event loop!
// ❌ Don't crash on expected errors
// Only crash on unrecoverable errors
Summary Table
| Pattern | Use Case | Example |
|---|---|---|
| try-catch | Synchronous code | try { JSON.parse() } catch {} |
| Error-first callback | Legacy Node.js APIs | (err, data) => {} |
| Promise.catch() | Promise chains | .then().catch() |
| async/await + try-catch | Modern async code | try { await fn() } catch {} |
| uncaughtException | Last-resort handler | process.on('uncaughtException') |
| Express middleware | HTTP error responses | app.use((err, req, res, next) => {}) |
Conclusion
Robust error handling is what separates production applications from prototypes:
- Use async/await with try-catch for modern code
- Create custom error classes for domain-specific errors
- Handle errors at the right level — don't catch what you can't handle
- Log with context — error messages alone aren't enough
- Always have global handlers for uncaught exceptions and rejections
An unhandled error in Node.js can crash your server. Handle errors deliberately and your application will be resilient.
#nodejs #error-handling #javascript #async #backend #web-development