文章

异步错误处理机制深度解析

异步错误处理机制深度解析

深入理解 JavaScript 异步错误处理机制,掌握 try-catch、Promise.catch、全局错误监听等多种捕获方式,构建健壮的异步代码。

一、背景与问题

异步编程中的错误处理是前端开发的重要难点:

1
2
3
4
5
6
7
8
// ❌ 常见错误:try-catch 无法捕获异步错误
try {
  setTimeout(() => {
    throw new Error('异步错误');  // 无法被捕获!
  }, 100);
} catch (error) {
  console.error(error);  // 永远不会执行
}

核心问题

  1. try-catch 只能捕获同步错误
  2. Promise 错误会静默失败(不处理时)
  3. async/await 错误处理方式多样
  4. 全局错误监听机制复杂

二、核心概念与定义

2.1 同步错误 vs 异步错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 同步错误:立即抛出,可以被 try-catch 捕获
try {
  throw new Error('同步错误');
} catch (error) {
  console.log('捕获成功:', error.message);
}

// 异步错误:在未来的调用栈中抛出,无法被当前 try-catch 捕获
try {
  setTimeout(() => {
    throw new Error('异步错误');  // 调用栈已不同
  }, 0);
} catch (error) {
  console.log('不会执行');
}

2.2 错误冒泡机制

Promise 中的错误会沿着 Promise 链向下冒泡,直到遇到 .catch()

1
2
3
4
5
6
7
Promise.resolve()
  .then(() => { throw new Error('错误1'); })  // 抛出
  .then(() => { /* 不会执行 */ })             // 跳过
  .then(() => { /* 不会执行 */ })             // 跳过
  .catch(error => {                           // 捕获
    console.log(error.message);  // '错误1'
  });

2.3 错误类型

错误类型触发场景捕获方式
Error通用错误try-catch / .catch()
TypeError类型错误try-catch / .catch()
ReferenceError引用错误try-catch / .catch()
SyntaxError语法错误无法捕获(解析阶段)
AggregateError多个错误集合Promise.any() 失败

三、最小示例

3.1 Promise 错误处理

1
2
3
4
5
6
7
8
9
10
11
12
// 方式一:.catch() 方法
fetch('/api/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('请求失败:', error));

// 方式二:then 的第二个参数
fetch('/api/data')
  .then(
    response => response.json(),
    error => console.error('请求失败:', error)  // 捕获 fetch 错误
  );

3.2 async/await 错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 方式一:try-catch
async function fetchData() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('获取数据失败:', error);
    throw error;  // 可以选择重新抛出
  }
}

// 方式二:.catch() 链式调用
async function fetchData() {
  const data = await fetch('/api/data')
    .then(r => r.json())
    .catch(error => {
      console.error('获取数据失败:', error);
      return null;  // 返回默认值
    });
  return data;
}

四、核心知识点拆解

4.1 try-catch 的作用域限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ❌ 错误示范:无法捕获回调中的错误
try {
  [1, 2, 3].forEach(num => {
    if (num === 2) throw new Error('错误');
  });
} catch (error) {
  console.log('捕获:', error);  // 可以捕获!forEach 是同步的
}

// ❌ 错误示范:无法捕获异步回调中的错误
try {
  setTimeout(() => {
    throw new Error('异步错误');  // 无法捕获
  }, 0);
} catch (error) {
  console.log('不会执行');
}

// ✅ 正确方式:在回调内部捕获
setTimeout(() => {
  try {
    throw new Error('异步错误');
  } catch (error) {
    console.log('捕获:', error);
  }
}, 0);

4.2 Promise 错误冒泡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 错误会沿着 Promise 链冒泡
const promise = new Promise((resolve, reject) => {
  reject(new Error('初始错误'));
});

promise
  .then(value => {
    console.log('不会执行');
    return value;
  })
  .then(value => {
    console.log('不会执行');
    return value;
  })
  .catch(error => {
    console.log('捕获:', error.message);  // '初始错误'
    return '恢复值';  // 返回值会传递给后续 then
  })
  .then(value => {
    console.log('恢复后:', value);  // '恢复值'
  });

4.3 async/await 错误处理模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 模式一:包装函数
async function safeAsync(fn) {
  try {
    return [null, await fn()];
  } catch (error) {
    return [error, null];
  }
}

// 使用(类似 Go 语言风格)
const [error, data] = await safeAsync(() => fetchData());
if (error) {
  console.error(error);
} else {
  console.log(data);
}

// 模式二:错误边界函数
function withErrorHandler(fn, handler) {
  return async (...args) => {
    try {
      return await fn(...args);
    } catch (error) {
      return handler(error, ...args);
    }
  };
}

const safeFetch = withErrorHandler(
  fetchData,
  (error) => {
    console.error(error);
    return { data: null, error };
  }
);

4.4 全局错误监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 1. window.onerror(传统方式)
window.onerror = (message, source, lineno, colno, error) => {
  console.error('全局错误:', { message, source, lineno, colno, error });
  return true;  // 阻止默认行为
};

// 2. window.addEventListener('error')
window.addEventListener('error', (event) => {
  console.error('错误事件:', event.error);
});

// 3. Promise 未捕获错误
window.addEventListener('unhandledrejection', (event) => {
  console.error('未处理的 Promise 拒绝:', event.reason);
  event.preventDefault();  // 阻止控制台警告
});

// 4. Node.js 环境
process.on('uncaughtException', (error) => {
  console.error('未捕获异常:', error);
});

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

五、实战案例

5.1 统一错误处理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class ErrorHandler {
  constructor(options = {}) {
    this.handlers = new Map();
    this.fallback = options.fallback || console.error;
  }

  // 注册错误处理器
  register(errorType, handler) {
    this.handlers.set(errorType, handler);
    return this;
  }

  // 处理错误
  handle(error) {
    // 查找匹配的处理器
    for (const [ErrorType, handler] of this.handlers) {
      if (error instanceof ErrorType) {
        return handler(error);
      }
    }
    // 使用默认处理器
    return this.fallback(error);
  }

  // 包装异步函数
  wrap(fn) {
    return async (...args) => {
      try {
        return await fn(...args);
      } catch (error) {
        return this.handle(error);
      }
    };
  }
}

// 使用示例
const errorHandler = new ErrorHandler()
  .register(TypeError, (e) => console.warn('类型错误:', e.message))
  .register(ReferenceError, (e) => console.warn('引用错误:', e.message));

const safeFn = errorHandler.wrap(async () => {
  const data = await fetchData();
  return data.name.toUpperCase();  // 可能抛出 TypeError
});

5.2 API 请求错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class ApiClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
  }

  async request(path, options = {}) {
    try {
      const response = await fetch(`${this.baseURL}${path}`, options);
      
      // 检查 HTTP 状态
      if (!response.ok) {
        throw new HttpError(response.status, response.statusText);
      }
      
      return await response.json();
    } catch (error) {
      // 网络错误
      if (error instanceof TypeError && error.message.includes('fetch')) {
        throw new NetworkError('网络请求失败');
      }
      // 重新抛出
      throw error;
    }
  }

  async get(path) {
    return this.request(path, { method: 'GET' });
  }

  async post(path, data) {
    return this.request(path, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
  }
}

// 自定义错误类型
class HttpError extends Error {
  constructor(status, statusText) {
    super(`HTTP ${status}: ${statusText}`);
    this.name = 'HttpError';
    this.status = status;
  }
}

class NetworkError extends Error {
  constructor(message) {
    super(message);
    this.name = 'NetworkError';
  }
}

5.3 React 错误边界

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // 上报错误
    console.error('组件错误:', error, errorInfo);
    // 可以发送到错误监控服务
    // reportError(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div>
          <h1>出错了</h1>
          <button onClick={() => this.setState({ hasError: false })}>
            重试
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

// 使用
<ErrorBoundary fallback={<div>加载失败</div>}>
  <MyComponent />
</ErrorBoundary>

六、底层原理

6.1 错误对象的创建与传播

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Error 对象结构
const error = new Error('错误信息');
console.log(error);
// {
//   name: 'Error',
//   message: '错误信息',
//   stack: 'Error: 错误信息\n    at <anonymous>:1:15'
// }

// 错误传播:调用栈
function a() {
  b();
}
function b() {
  c();
}
function c() {
  throw new Error('错误');
}

try {
  a();
} catch (error) {
  console.log(error.stack);
  // Error: 错误
  //     at c (<anonymous>:9:11)
  //     at b (<anonymous>:6:5)
  //     at a (<anonymous>:3:5)
  //     at <anonymous>:13:5
}

6.2 Promise 错误处理机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Promise 内部错误处理
new Promise((resolve, reject) => {
  // 同步抛出错误会被捕获并转为 reject
  throw new Error('错误');
})
  .catch(error => {
    console.log('捕获:', error.message);
  });

// 等价于
new Promise((resolve, reject) => {
  reject(new Error('错误'));
})
  .catch(error => {
    console.log('捕获:', error.message);
  });

6.3 微任务队列中的错误

1
2
3
4
5
6
7
8
// Promise.then 中的错误
Promise.resolve()
  .then(() => {
    throw new Error('微任务错误');
  });

// 错误在微任务队列中传播
// 如果没有 .catch(),会在微任务队列清空后触发 unhandledrejection

七、高频面试题解析

Q1:为什么 try-catch 无法捕获 setTimeout 中的错误?

答案:因为 setTimeout 的回调在新的调用栈中执行,try-catch 只能捕获同一调用栈中的错误。

1
2
3
4
// 调用栈示意
// main() -> try -> setTimeout(注册) -> catch(已退出)
// ... 时间流逝 ...
// timer() -> callback() -> throw Error  // 新调用栈,无 catch

Q2:Promise 的错误会静默失败吗?

答案:会。如果 Promise 被 reject 且没有 .catch() 处理,错误会静默失败(现代浏览器会触发 unhandledrejection 事件)。

1
2
3
4
5
6
// ❌ 错误会静默失败
Promise.reject(new Error('错误'));

// ✅ 正确做法
Promise.reject(new Error('错误'))
  .catch(error => console.error(error));

Q3:如何实现 Go 风格的错误处理?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Go 风格:返回 [error, value]
async function go(asyncFn) {
  try {
    const result = await asyncFn();
    return [null, result];
  } catch (error) {
    return [error, null];
  }
}

// 使用
const [err, data] = await go(() => fetchData());
if (err) {
  console.error(err);
  return;
}
console.log(data);

Q4:async 函数返回的 Promise 如何处理错误?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async function fn() {
  throw new Error('错误');
}

// 方式一:await + try-catch
try {
  await fn();
} catch (error) {
  console.error(error);
}

// 方式二:.catch()
await fn().catch(error => console.error(error));

// 方式三:不 await,让错误冒泡
return fn();  // 错误会传递给调用者

八、总结与扩展

核心要点

  1. try-catch 限制:只能捕获同步错误,无法捕获异步回调中的错误
  2. Promise 错误冒泡:错误沿 Promise 链向下传播,直到遇到 .catch()
  3. async/await:可以用 try-catch 捕获 await 表达式的错误
  4. 全局监听unhandledrejection 捕获未处理的 Promise 错误

最佳实践

  1. 永远处理 Promise 错误:每个 Promise 链末尾加 .catch()
  2. 统一错误类型:自定义 HttpError、NetworkError 等
  3. 错误上报:全局错误监听 + 上报服务
  4. 优雅降级:错误时显示友好提示,而非白屏

扩展阅读

  • Error Cause:ES2022 错误链 new Error('message', { cause: originalError })
  • AggregateErrorPromise.any() 失败时返回多个错误
  • Sentry:前端错误监控平台
  • Error Boundary:React 组件级错误处理

掌握异步错误处理机制,是编写健壮前端代码的基础能力。

本文由作者按照 CC BY 4.0 进行授权