您现在的位置是:网站首页 > 异步错误处理文章详情

异步错误处理

异步错误处理的挑战

JavaScript的异步特性带来了独特的错误处理难题。回调函数、Promise和async/await各自有不同的错误传播机制。传统的try-catch在异步上下文中无法捕获回调函数内部的错误,这导致许多开发者不得不采用深度嵌套的错误优先回调模式。

// 传统回调的错误处理
fs.readFile('nonexistent.txt', (err, data) => {
  if (err) {
    console.error('读取文件出错:', err.message);
    return;
  }
  console.log(data);
});

Promise的错误捕获

Promise通过.catch()方法提供了更优雅的错误处理方式。当Promise链中的任何环节抛出错误或返回rejected状态的Promise时,控制权会传递给最近的.catch()处理器。这种机制避免了回调地狱,使错误处理流程更加线性。

fetch('https://api.example.com/data')
  .then(response => {
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return response.json();
  })
  .then(data => processData(data))
  .catch(error => {
    console.error('请求失败:', error);
    // 可以在这里进行错误恢复或上报
    reportErrorToServer(error);
  });

async/await的try-catch模式

async/await语法让异步代码看起来像同步代码,错误处理也可以使用传统的try-catch结构。这种模式更符合直觉,特别是在需要处理多个异步操作时,能显著提升代码可读性。

async function fetchUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    const user = await response.json();
    const posts = await fetch(`/api/posts?userId=${user.id}`);
    return { user, posts };
  } catch (error) {
    console.error('获取用户数据失败:', error);
    // 可以在这里决定是继续抛出错误还是返回默认值
    return { user: null, posts: [] };
  }
}

未处理的Promise拒绝

Node.js和浏览器环境对未处理的Promise拒绝有不同的处理方式。在Node.js中,未处理的拒绝会导致进程退出,而在浏览器中可能只是静默失败。应该始终为Promise添加错误处理,或者使用全局捕获机制。

// 浏览器环境
window.addEventListener('unhandledrejection', event => {
  console.warn(`未处理的Promise拒绝: ${event.reason}`);
  event.preventDefault(); // 阻止默认行为(如控制台报错)
});

// Node.js环境
process.on('unhandledRejection', (reason, promise) => {
  console.error('未处理的拒绝:', reason);
});

错误边界与传播策略

在复杂的异步操作中,需要决定错误应该在何处被捕获和处理。有时需要在局部处理错误并继续执行,有时则需要让错误冒泡到更高层级。设计良好的错误传播策略对维护大型应用至关重要。

async function processOrder(orderId) {
  try {
    const order = await getOrder(orderId);
    await validateOrder(order);
    await processPayment(order);
    await updateInventory(order);
    await sendConfirmation(order);
  } catch (error) {
    if (error instanceof PaymentError) {
      await notifyPaymentFailure(order, error);
    } else if (error instanceof InventoryError) {
      await scheduleRetry(order, error);
    } else {
      // 其他未知错误向上传播
      throw error;
    }
  }
}

错误类型与自定义错误

创建特定的错误类型有助于更精确地处理不同类别的错误。自定义错误可以携带额外的上下文信息,帮助调试和错误恢复。

class NetworkError extends Error {
  constructor(url, status, ...params) {
    super(...params);
    this.name = 'NetworkError';
    this.url = url;
    this.status = status;
    this.timestamp = new Date();
  }
}

async function fetchWithRetry(url, retries = 3) {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new NetworkError(url, response.status, '请求失败');
    }
    return response.json();
  } catch (error) {
    if (retries > 0 && error instanceof NetworkError) {
      await delay(1000);
      return fetchWithRetry(url, retries - 1);
    }
    throw error;
  }
}

并行操作的错误处理

当使用Promise.all或Promise.race等组合器处理多个并行异步操作时,错误处理行为会有所不同。Promise.all会在任何一个Promise拒绝时立即拒绝,而Promise.any会在所有Promise都拒绝时才拒绝。

async function fetchMultipleResources(urls) {
  try {
    // 所有请求必须成功
    const responses = await Promise.all(urls.map(url => fetch(url)));
    return await Promise.all(responses.map(r => r.json()));
  } catch (error) {
    // 任何一个请求失败都会进入这里
    console.error('部分资源加载失败', error);
    throw new PartialDataError('未能获取全部数据');
  }
}

async function fetchFromFastestMirror(mirrors) {
  try {
    // 使用Promise.any获取最快的可用镜像
    const response = await Promise.any(
      mirrors.map(url => fetch(url).then(r => {
        if (!r.ok) throw new Error(`镜像不可用: ${url}`);
        return r;
      })
    );
    return response.json();
  } catch (error) {
    // 所有镜像都不可用时进入这里
    throw new NoAvailableMirrorError('所有镜像均不可用');
  }
}

错误恢复与重试机制

对于暂时性错误(如网络波动),实现自动重试机制可以提升用户体验。指数退避算法是常用的重试策略,避免对服务器造成过大压力。

async function fetchWithExponentialBackoff(url, maxRetries = 5) {
  let attempt = 0;
  while (true) {
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return await response.json();
    } catch (error) {
      if (attempt >= maxRetries) throw error;
      
      const delayMs = Math.pow(2, attempt) * 1000 + Math.random() * 1000;
      console.warn(`请求失败,${delayMs}ms后重试... (${attempt + 1}/${maxRetries})`);
      await new Promise(resolve => setTimeout(resolve, delayMs));
      attempt++;
    }
  }
}

错误日志与监控

在生产环境中,需要将客户端错误记录到服务器以便分析。要注意避免敏感信息泄露,同时确保错误上报本身不会引发新的错误。

async function logError(error, context = {}) {
  try {
    const errorData = {
      message: error.message,
      stack: error.stack,
      name: error.name,
      timestamp: new Date().toISOString(),
      userAgent: navigator.userAgent,
      ...context
    };
    
    await fetch('/api/error-log', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(errorData)
    });
  } catch (reportingError) {
    console.error('无法上报错误:', reportingError);
  }
}

window.addEventListener('error', event => {
  logError(event.error, { type: 'unhandled' });
});

window.addEventListener('unhandledrejection', event => {
  logError(event.reason, { type: 'promise-rejection' });
});

测试异步错误

编写测试用例验证错误处理逻辑的正确性同样重要。现代测试框架提供了专门的方法来测试异步代码中的错误抛出。

// 使用Jest测试异步错误
describe('fetchUserData', () => {
  it('应该在API返回404时抛出NotFoundError', async () => {
    fetch.mockRejectedValueOnce(new Error('404'));
    await expect(fetchUserData(123)).rejects.toThrow(NotFoundError);
  });

  it('应该在网络错误时自动重试3次', async () => {
    fetch.mockRejectedValueOnce(new Error('network'));
    fetch.mockRejectedValueOnce(new Error('network'));
    fetch.mockResolvedValueOnce({ ok: true, json: () => ({ id: 123 }) });
    
    const result = await fetchUserData(123);
    expect(result.id).toBe(123);
    expect(fetch).toHaveBeenCalledTimes(3);
  });
});

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步