阿里云主机折上折
  • 微信号
Current Site:Index > Error handling strategy

Error handling strategy

Author:Chuan Chen 阅读数:49929人阅读 分类: Node.js

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:

  1. Prioritize checking the err parameter
  2. Use return to terminate subsequent logic execution
  3. 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 and exit 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 manual pipe
  • 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

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.