14.2 异步栈追踪与 Error.captureStackTrace

异步栈追踪问题

JavaScript 的异步特性使得错误堆栈追踪变得复杂,传统的错误堆栈在异步操作中往往会丢失重要的上下文信息。

同步错误堆栈示例

function a() {
  throw new Error('Sync error');
}

function b() {
  a();
}

function c() {
  b();
}

try {
  c();
} catch (err) {
  console.log(err.stack);
  /*
  Error: Sync error
      at a (file.js:2:9)
      at b (file.js:6:3)
      at c (file.js:10:3)
      at file.js:14:3
  */
}

异步错误堆栈问题

function a() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error('Async error'));
    }, 100);
  });
}

async function b() {
  await a();
}

async function c() {
  await b();
}

c().catch(err => {
  console.log(err.stack);
  /*
  Error: Async error
      at Timeout._onTimeout (file.js:3:14)
  */
  // 丢失了 b() 和 c() 的调用信息
});

Error.captureStackTrace

Node.js 提供了 Error.captureStackTrace 方法来定制错误对象的堆栈跟踪信息。

基本用法

function MyError(message) {
  Error.captureStackTrace(this, MyError);
  this.message = message;
  this.name = 'MyError';
}

// 继承 Error.prototype
MyError.prototype = Object.create(Error.prototype);
MyError.prototype.constructor = MyError;

// 使用示例
function a() {
  throw new MyError('Something went wrong');
}

function b() {
  a();
}

try {
  b();
} catch (err) {
  console.log(err.stack);
  /*
  MyError: Something went wrong
      at a (file.js:10:9)
      at b (file.js:14:3)
      at file.js:18:3
  */
  // 注意:MyError 构造函数本身被排除在堆栈外
}

参数说明

Error.captureStackTrace(targetObject[, constructorOpt])

  1. targetObject - 要添加堆栈跟踪的目标对象
  2. constructorOpt - 可选的构造函数,堆栈跟踪将从这个函数以上的调用帧开始

异步堆栈追踪增强

Node.js 的异步堆栈追踪

Node.js 8+ 开始支持异步堆栈追踪,但需要启用特定标志或使用最新版本:

node --async-stack-traces your-script.js

实际示例

async function taskA() {
  await new Promise(resolve => setTimeout(resolve, 100));
  throw new Error('Task failed');
}

async function taskB() {
  await taskA();
}

async function taskC() {
  await taskB();
}

// 在 Node.js 12+ 中
taskC().catch(err => {
  console.log(err.stack);
  /*
  Error: Task failed
      at taskA (/path/to/file.js:3:9)
      at async taskB (/path/to/file.js:7:3)
      at async taskC (/path/to/file.js:11:3)
  */
});

手动增强异步堆栈

对于不支持异步堆栈的环境,可以手动捕获和合并堆栈:

class AsyncError extends Error {
  constructor(message, asyncContext) {
    super(message);
    this.name = 'AsyncError';
    
    if (asyncContext && asyncContext.stack) {
      this.stack = `${this.stack}\n--- async context ---\n${asyncContext.stack}`;
    }
  }
}

async function withAsyncStack(fn) {
  const stackHolder = { stack: '' };
  Error.captureStackTrace(stackHolder, withAsyncStack);
  
  try {
    return await fn();
  } catch (err) {
    throw new AsyncError(err.message, stackHolder);
  }
}

// 使用示例
async function operationA() {
  await new Promise(resolve => setTimeout(resolve, 50));
  throw new Error('Database query failed');
}

async function operationB() {
  return withAsyncStack(async () => {
    await operationA();
  });
}

operationB().catch(err => {
  console.log(err.stack);
  /*
  AsyncError: Database query failed
      at withAsyncStack (file.js:15:11)
      at async operationB (file.js:22:10)
  --- async context ---
      at withAsyncStack (file.js:10:25)
      at operationB (file.js:21:10)
      at file.js:26:1
  */
});

实际应用场景

1. API 客户端错误追踪

class ApiError extends Error {
  constructor(message, requestInfo) {
    super(message);
    this.name = 'ApiError';
    this.requestInfo = requestInfo;
    
    // 捕获当前堆栈,排除构造函数
    Error.captureStackTrace(this, ApiError);
    
    // 如果是异步操作,可以添加异步上下文
    if (requestInfo.asyncStack) {
      this.stack += `\n--- API call context ---\n${requestInfo.asyncStack}`;
    }
  }
}

async function makeApiCall(url) {
  const asyncStack = { stack: '' };
  Error.captureStackTrace(asyncStack, makeApiCall);
  
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return response.json();
  } catch (err) {
    throw new ApiError(
      `Failed to call API: ${err.message}`,
      {
        url,
        asyncStack,
        timestamp: new Date().toISOString()
      }
    );
  }
}

// 使用示例
async function getUserData() {
  try {
    return await makeApiCall('https://api.example.com/user');
  } catch (err) {
    if (err instanceof ApiError) {
      console.error('API Error:', {
        message: err.message,
        stack: err.stack,
        request: err.requestInfo
      });
    }
    throw err;
  }
}

2. 数据库操作追踪

class DatabaseError extends Error {
  constructor(message, query, parameters) {
    super(message);
    this.name = 'DatabaseError';
    this.query = query;
    this.parameters = parameters;
    
    // 捕获堆栈并添加SQL上下文
    Error.captureStackTrace(this, DatabaseError);
    this.stack += `\n--- SQL Query ---\n${query}\n--- Parameters ---\n${JSON.stringify(parameters)}`;
  }
}

async function queryDatabase(sql, params) {
  const stackHolder = { stack: '' };
  Error.captureStackTrace(stackHolder, queryDatabase);
  
  try {
    // 模拟数据库操作
    if (sql.includes('error')) {
      throw new Error('Syntax error in SQL');
    }
    return { rows: [] };
  } catch (err) {
    throw new DatabaseError(
      `Database operation failed: ${err.message}`,
      sql,
      params,
      stackHolder.stack
    );
  }
}

// 使用示例
async function getUsers() {
  try {
    return await queryDatabase('SELECT * FROM users WHERE error', []);
  } catch (err) {
    console.error(err.stack);
    /*
    DatabaseError: Database operation failed: Syntax error in SQL
        at queryDatabase (file.js:15:11)
        at async getUsers (file.js:25:12)
    --- SQL Query ---
    SELECT * FROM users WHERE error
    --- Parameters ---
    []
    */
  }
}

3. 复杂工作流错误追踪

class WorkflowError extends Error {
  constructor(message, step, context) {
    super(message);
    this.name = 'WorkflowError';
    this.step = step;
    this.context = context;
    
    // 捕获堆栈并添加上下文
    Error.captureStackTrace(this, WorkflowError);
    
    if (context.asyncStack) {
      this.stack += `\n--- Workflow Context ---\nStep: ${step}\n${context.asyncStack}`;
    }
  }
}

async function executeWorkflow() {
  const workflowStack = { stack: '' };
  Error.captureStackTrace(workflowStack, executeWorkflow);
  
  const context = {
    userId: 123,
    asyncStack: workflowStack.stack
  };
  
  try {
    await step1(context);
    await step2(context);
    await step3(context);
  } catch (err) {
    if (!(err instanceof WorkflowError)) {
      throw new WorkflowError(
        `Workflow failed: ${err.message}`,
        'unknown',
        context
      );
    }
    throw err;
  }
}

async function step1(context) {
  try {
    // 模拟操作
    if (Math.random() > 0.5) {
      throw new Error('Random step1 error');
    }
  } catch (err) {
    throw new WorkflowError(
      `Step1 failed: ${err.message}`,
      'step1',
      context
    );
  }
}

// 使用示例
executeWorkflow().catch(err => {
  console.error('Workflow execution failed:', err.stack);
  /*
  WorkflowError: Step1 failed: Random step1 error
      at step1 (file.js:35:11)
      at async executeWorkflow (file.js:20:5)
  --- Workflow Context ---
  Step: step1
      at executeWorkflow (file.js:10:25)
      at file.js:40:1
  */
});

最佳实践

  1. 合理使用 Error.captureStackTrace

    • 在自定义错误类中使用
    • 指定适当的 constructorOpt 排除不必要的信息
  2. 异步错误处理

    • 在 Node.js 中启用异步堆栈追踪 (--async-stack-traces)
    • 对于关键异步操作,手动捕获和保存上下文
  3. 错误信息丰富化

    • 添加相关上下文信息(请求参数、时间戳等)
    • 保持堆栈信息清晰可读
  4. 性能考虑

    • 堆栈捕获有一定性能开销,避免在热点路径过度使用
    • 生产环境可以考虑限制堆栈深度
  5. 跨环境兼容

    • 浏览器和 Node.js 的堆栈行为可能不同
    • 考虑使用 source maps 映射压缩代码的错误位置

总结

  1. 异步栈追踪是现代 JavaScript 开发中的重要调试工具
  2. Error.captureStackTrace 提供了自定义堆栈跟踪的能力
  3. 手动增强异步上下文可以在不支持异步堆栈的环境中提供更好的调试信息
  4. 丰富的错误上下文可以显著提高错误诊断效率
  5. 合理使用堆栈捕获功能,平衡调试需求和性能影响

通过掌握这些技术,开发者可以构建更健壮、更易于调试的 JavaScript 应用程序,特别是在复杂的异步场景中能够快速定位和解决问题。

#前端开发 分享于 2025-03-25

【 内容由 AI 共享,不代表本站观点,请谨慎参考 】