Error handling strategy
Error Handling Strategies
Node.js, as an asynchronous event-driven runtime, has a significantly different error handling mechanism compared to traditional synchronous programming. A reasonable error handling strategy can improve application stability and prevent uncaught exceptions from causing process crashes. Below are common error handling patterns and practical solutions in Node.js.
Error Handling in Callback Functions
Early versions of Node.js widely adopted the error-first callback pattern, where asynchronous functions pass error objects through the first parameter of the callback:
const fs = require('fs');
fs.readFile('/nonexistent/file.txt', (err, data) => {
if (err) {
console.error('Failed to read file:', err.message);
return;
}
console.log('File content:', data.toString());
});
Typical error handling patterns include:
- Prioritize checking the
err
parameter - Use
return
to terminate subsequent logic execution - Error messages should include sufficient context (e.g., file path)
Promise Error Handling
Modern Node.js recommends using Promise chaining with .catch()
to capture exceptions:
const fs = require('fs').promises;
fs.readFile('/nonexistent/file.txt')
.then(data => console.log('File content:', data.toString()))
.catch(err => {
console.error('Operation failed:', err.stack);
// Fallback data can be returned
return Buffer.from('Default content');
});
Key practices:
- Every Promise chain must include a catch handler
- Error bubbling can be achieved by rethrowing:
throw new Error('Wrapped error')
- Be mindful of partial failure scenarios when using
Promise.all
try-catch with async/await
When using async functions, try-catch is the most intuitive error handling approach:
async function processFile() {
try {
const data = await fs.readFile('/nonexistent/file.txt');
const processed = await transformData(data);
return processed;
} catch (err) {
if (err.code === 'ENOENT') {
// Handle specific error types
await createDefaultFile();
} else {
// Rethrow other errors
throw err;
}
}
}
Considerations:
- Avoid excessive nesting of try-catch blocks
- Distinguish between recoverable errors and fatal errors
- Asynchronous errors do not bubble up to outer synchronous code
Error Handling in Event Emitters
EventEmitters require listening for the error
event; otherwise, uncaught exceptions will be thrown:
const { EventEmitter } = require('events');
class MyEmitter extends EventEmitter {}
const emitter = new MyEmitter();
emitter.on('error', (err) => {
console.error('Emitter error:', err.message);
});
emitter.emit('error', new Error('Example error'));
Best practices:
- Critical event emitters must have an
error
listener - Error events should include state information when the error occurred
- Consider using the
domain
module for complex event streams (deprecated but still valuable for reference)
Process-Level Error Handling
Global error capture can prevent unexpected process termination:
process.on('uncaughtException', (err) => {
console.error('Uncaught exception:', err);
// Terminate the process after logging
process.exit(1);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection:', reason);
// Record Promise state here if needed
});
Key points:
- Terminate the process as soon as possible after
uncaughtException
- Use process managers like PM2 for automatic restarts
- Distinguish between development (detailed logs) and production (graceful degradation) environments
Error Classification and Custom Errors
Creating specific error types aids in error handling:
class DatabaseError extends Error {
constructor(message, query) {
super(message);
this.name = 'DatabaseError';
this.query = query;
this.stack = `${this.stack}\nQuery: ${query}`;
}
}
try {
throw new DatabaseError('Connection timeout', 'SELECT * FROM users');
} catch (err) {
if (err instanceof DatabaseError) {
console.error('Database operation failed:', err.query);
}
}
Recommended practices:
- Inherit from the
Error
base class to create business errors - Attach debugging information (e.g., SQL, request parameters)
- Use
err.code
to define machine-readable error codes
Logging Strategies
Effective error logs should include:
const winston = require('winston');
const logger = winston.createLogger({
level: 'error',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log' })
]
});
function handleError(err) {
logger.error({
message: err.message,
stack: err.stack,
context: {
userId: 123,
apiPath: '/v1/users'
}
});
}
Logging essentials:
- Structured logs (JSON format)
- Include call chain information (requestId)
- Sanitize sensitive information
- Differentiate error levels (error/warn/info)
HTTP Error Responses
Web services should return standardized error responses:
const express = require('express');
const app = express();
app.get('/api/data', async (req, res) => {
try {
const data = await fetchData();
res.json(data);
} catch (err) {
res.status(500).json({
error: 'SERVER_ERROR',
message: 'Server processing failed',
detail: process.env.NODE_ENV === 'development' ? err.stack : undefined
});
}
});
// 404 handling
app.use((req, res) => {
res.status(404).json({
error: 'NOT_FOUND',
message: `Path ${req.path} does not exist`
});
});
HTTP error standards:
- 4xx indicates client errors
- 5xx indicates server errors
- Error response bodies should include machine-readable error codes
- Hide stack traces in production environments
Retry Mechanisms
Implement automatic retries for transient errors:
async function withRetry(fn, maxAttempts = 3) {
let attempt = 0;
while (true) {
try {
return await fn();
} catch (err) {
if (!isRetriableError(err) || ++attempt >= maxAttempts) {
throw err;
}
await new Promise(r => setTimeout(r, 1000 * attempt));
}
}
}
function isRetriableError(err) {
return [
'ECONNRESET',
'ETIMEDOUT',
'ENOTFOUND'
].includes(err.code);
}
Retry strategy points:
- Use exponential backoff to avoid cascading failures
- Set a maximum number of retries
- Only retry transient errors like network timeouts
- Record retry counts for monitoring
Error Monitoring and Alerts
Integrate APM tools for real-time monitoring:
const Sentry = require('@sentry/node');
Sentry.init({
dsn: 'YOUR_DSN',
tracesSampleRate: 1.0,
attachStacktrace: true
});
// Manual exception capture
try {
riskyOperation();
} catch (err) {
Sentry.captureException(err, {
tags: { module: 'payment' },
extra: { invoiceId: 12345 }
});
throw err;
}
Monitoring system requirements:
- Error aggregation and classification
- Context information collection
- Threshold alerts (e.g., more than 50 errors of the same type per minute)
- Integration with ticketing systems
Testing Strategies
Error scenarios should be included in unit tests:
const assert = require('assert');
const { connectDB } = require('./db');
describe('Database connection', () => {
it('should handle authentication failure correctly', async () => {
await assert.rejects(
() => connectDB('wrong_credential'),
{
name: 'DatabaseError',
code: 'EAUTHFAIL'
}
);
});
it('should handle connection timeout', async function() {
this.timeout(5000); // Set test timeout
await assert.rejects(
() => connectDB({ host: '1.2.3.4', timeout: 100 }),
/ETIMEDOUT/
);
});
});
Testing essentials:
- Simulate network failures (using tools like nock)
- Validate error types and codes
- Include recovery logic tests
- Error handling under stress tests
Defensive Programming
Coding patterns to prevent errors:
function parseJSONSafely(input) {
if (typeof input !== 'string') {
throw new TypeError('Input must be a string');
}
try {
return JSON.parse(input);
} catch {
// Return null instead of throwing an exception
return null;
}
}
// Parameter validation
function createUser(userData) {
const required = ['name', 'email'];
const missing = required.filter(field => !userData[field]);
if (missing.length) {
throw new ValidationError(`Missing required fields: ${missing.join(', ')}`);
}
// ... Business logic
}
Defensive techniques:
- Input parameter type checking
- Set default values
- Use TypeScript for static type checking
- Add precondition assertions for critical operations
Error Handling Middleware
Centralized error handling in Express:
// Error class definition
class APIError extends Error {
constructor(message, status = 500) {
super(message);
this.status = status;
}
}
// Middleware
function errorHandler(err, req, res, next) {
if (res.headersSent) {
return next(err);
}
const status = err.status || 500;
res.status(status).json({
error: err.name || 'InternalError',
message: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
}
// Business route
app.get('/users/:id', (req, res, next) => {
const user = getUser(req.params.id);
if (!user) {
throw new APIError('User not found', 404);
}
res.json(user);
});
// Register middleware
app.use(errorHandler);
Middleware advantages:
- Unified error response format
- Avoid repetitive try-catch blocks
- Support categorized error handling
- Can integrate logging
Resource Cleanup
Ensure resources are properly released when errors occur:
async function withTempFile(fn) {
const tempPath = '/tmp/' + Math.random().toString(36).slice(2);
let handle;
try {
handle = await fs.open(tempPath, 'w');
return await fn(handle);
} finally {
if (handle) await handle.close();
try {
await fs.unlink(tempPath);
} catch (cleanupErr) {
console.error('Failed to clean up temp file:', cleanupErr);
}
}
}
Resource management principles:
- Use try-finally to ensure cleanup execution
- Use connection pools for database connections
- Implement the dispose pattern for resource lifecycle management
- Consider using AsyncResource to track asynchronous contexts
Error Handling and Transactions
Error handling example in database transactions:
async function transferFunds(senderId, receiverId, amount) {
const client = await pool.connect();
try {
await client.query('BEGIN');
const senderResult = await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2 RETURNING balance',
[amount, senderId]
);
if (senderResult.rows[0].balance < 0) {
throw new InsufficientFundError();
}
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
[amount, receiverId]
);
await client.query('COMMIT');
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
}
Transaction handling points:
- Ensure ROLLBACK is executed on error
- Place connection release in the finally block
- Handle special cases like deadlocks
- Consider using transaction retry decorators
Child Process Error Handling
Handling exceptions in child_process:
const { spawn } = require('child_process');
const child = spawn('ffmpeg', ['-i', 'input.mp4', 'output.avi']);
child.on('error', (err) => {
console.error('Failed to start child process:', err);
});
child.stderr.on('data', (data) => {
console.error('FFmpeg error output:', data.toString());
});
child.on('exit', (code, signal) => {
if (code !== 0) {
console.error(`Child process exited abnormally code=${code} signal=${signal}`);
}
});
Child process considerations:
- Listen for
error
andexit
events - Handle standard error output
- Set timeouts to terminate hanging processes
- Check exit codes when using
execFile
Error Handling and Streams
Error handling patterns for Node.js streams:
const fs = require('fs');
const zlib = require('zlib');
fs.createReadStream('input.txt')
.on('error', err => {
console.error('Read failed:', err.message);
})
.pipe(zlib.createGzip())
.on('error', err => {
console.error('Compression failed:', err.message);
})
.pipe(fs.createWriteStream('output.txt.gz'))
.on('error', err => {
console.error('Write failed:', err.message);
});
Stream handling key points:
- Each stream should have its own
error
listener - Use
pipeline
instead of manualpipe
- Handle backpressure-induced errors
- Consider stream state when implementing retry logic
Performance Considerations
Impact of error handling on performance:
// Inefficient approach (frequent error object creation)
function validateInput(input) {
if (!input) {
throw new Error('Input cannot be empty');
}
// ...
}
// Optimized solution (predefined error instance)
const EMPTY_INPUT_ERROR = Object.freeze(new Error('Input cannot be empty'));
function validateInputOptimized(input) {
if (!input) {
throw EMPTY_INPUT_ERROR;
}
// ...
}
// Performance test
console.time('Original version');
for (let i = 0; i < 1e6; i++) {
try { validateInput(null); } catch {}
}
console.timeEnd('Original version');
console.time('Optimized version');
for (let i = 0; i < 1e6; i++) {
try { validateInputOptimized(null); } catch {}
}
console.timeEnd('Optimized version');
Performance optimization directions:
- Avoid creating error objects in hot paths
- Reduce stack trace generation (
Error.captureStackTrace
) - Use
process.emitWarning
for non-fatal errors - Consider returning error codes instead of exceptions for high-frequency operations
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:async/await语法糖
下一篇:回调地狱问题与解决方案