常见的错误来源:
- 网络请求失败
- 用户输入非预期的值
- 接口的异常响应
- 外部服务的异常
本次分享有什么:
第一部分:核心概念与语法
- Try/Catch
- 不同类型的错误
- 如何创建一个自定义的错误类
- 如何在同步和异步代码中处理错误
- 与FetchAPI相关的错误处理
第二部分:最佳实践
- 什么时候向用户展示错误
- 如何展示错误
- 如何实现合适的错误日志输出
- 如何避免一些常见错误
第一部分
Error对象和try catch

在JavaScript中,当出现错误时,语言会抛出一个Error对象,Error是JavaScript语言中的内置的构造器对象。

我们可以通过实例化Error类来创建我们自己的错误对象(通用)。

javascript
throw new Error('User not found!') // 抛出一个错误,js程序将会中止执行
console.log('continus') // 不会预期输出
抛出一个错误时,js程序将会中止执行,我们可以使用try...catch 块来包裹那些可能抛出错误的代码

javascript
try{
throw new Error('User not found!')
}catch(error){
// 处理错误
console.log(error.message) // User not found!
}finally{
console.log('是否有错误都将执行') // 是否有错误都将执行
}
console.log('continus') // continus
内置的8种错误类型

最常见的错误类型是:
- ReferenceError 无效引用

- RangeError 范围错误

- SyntaxError 语法错误

- TypeError 类型错误


创建自定义错误类型
首先可以考虑下,为什么需要创建自定义错误类型?创建自定义类型的原因是什么?先看完下方的代码,我们再来思考这个问题:
javascript
class PaymentError extends Error{
constructor(code,message){
super(message);
this.name = 'PaymentError'
this.code = code
const codeMsgMap = {
'insufficient_funds':'已有金额不足以完成支付!',
'invalid_card': '卡号不正确!'
}
this.userMessage = codeMsgMap[code] || '支付时发生了一个错误!'
}
}
function makePayment(){
const availableFunds = 50;
const payment = 60;
if(payment>availableFunds){
throw new PaymentError('insufficient_funds',`待支付${payment},可用资金为${availableFunds}!`)
}
return '支付成功!'
}
try {
makePayment()
} catch (error) {
if(error instanceof PaymentError){ // 使用instanceof 区分自定义错误还是内置错误
if(error.code==='insufficient_funds'){
alert(error.userMessage) // to users
console.log(error.message) // to developers
}
if(error.code==='invalid_card'){
alert(error.userMessage)
console.log(error.message)
}
console.log(error.message) // PaymentError其他情况错误
}else{
console.log(error.message) // 内置错误
}
}
上述的PaymentError 是一个自定义的支付场景的错误,携带了更多的上下文信息,可以进行更准确的错误处理和友好提示。在业界主要的应用场景有:
- 领域特定错误处理
- 为特定业务逻辑创建专门的错误类型(如
**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">PaymentError</font>**、**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">AuthError</font>**)
- 为特定业务逻辑创建专门的错误类型(如
- 增强错误信息
- 添加额外的上下文信息(如请求ID、用户ID、业务状态码),示例:
<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">new DatabaseError('Connection failed', { host, port })</font>
- 添加额外的上下文信息(如请求ID、用户ID、业务状态码),示例:
- 错误分类处理
- 通过
**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">instanceof</font>**进行错误类型判断 - 实现精细化的错误处理逻辑
- 通过
- API库设计
- 公开明确的错误类型供使用者捕获处理
- 提高代码的可维护性和可预测性
- 错误监控与日志相关
- 便于错误分类统计
- 实现更精细的错误上报
实际实践案例:
- Stripe API 中的错误类型体系
- Node.js 核心模块中的
**<font style="color:rgb(64, 64, 64);">SystemError</font>** - Express/Koa 框架 中的
**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">HttpError</font>**
在异步代码中捕获错误
javascript
async function makePayment(){
const availableFunds = 50;
const payment = 60;
if(payment>availableFunds){
throw new PaymentError('insufficient_funds',`待支付${payment},可用资金为${availableFunds}!`)
}
return '支付成功!'
}
try {
await makePayment() // 如果没有await 则无法捕获错误
} catch (error) {
if(error instanceof PaymentError){ // 使用instanceof 区分自定义错误还是内置错误
if(error.code==='insufficient_funds'){
alert(error.userMessage) // to users
console.log(error.message) // to developers
}
if(error.code==='invalid_card'){
alert(error.userMessage)
console.log(error.message)
}
console.log(error.message) // PaymentError其他情况错误
}else{
console.log(error.message) // 内置错误
}
}
async异步函数中的错误,需要await 才可以被捕获到。
javascript
async function foo() {
return 42; // 等价于 Promise.resolve(42)
}
async function bar() {
throw new Error("Oops!"); // 等价于 Promise.reject(new Error("Oops!"))
}
原因在于async函数总是返回一个Promise,如果async函数内部抛出错误,他会返回一个被拒绝(rejected)的Promise。
await将暂停async函数执行,知道Promise完成(fulfilled 或 rejected)。如果 Promise 被拒绝(rejected) ,**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">await</font>** 会抛出错误 ,可以被 **<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">try/catch</font>** 捕获;如果 Promise 成功(fulfilled) ,**<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">await</font>** 返回解析后的值。
如果没有使用await,async 函数返回的是一个Pending Promise,在同步的try...catch中无法捕获这个错误,最终这个 Promise 的拒绝会变成 Unhandled Promise Rejection(未处理的 Promise 拒绝),可能导致程序崩溃。
javascript
async function fetchData() {
try {
const data = await someAsyncOperation();
console.log(data);
} catch (error) {
console.error("Caught error:", error);
}
}
与fetch相关的错误处理
javascript
async function fetchFun () {
try {
await fetch('https://testnotexist.com')
} catch (error) {
console.log('network-error',error)
}
}
fetchFun()

javascript
async function fetchFun () {
try {
await fetch('https://httpbin.org/status/404')
} catch (error) {
console.log('network-error',error)
}
}
fetchFun()

javascript
async function fetchFun () {
try {
await fetch('https://httpbin.org/status/404')
if(!res.ok){
throw new Error('Request failed!')
}
} catch (error) {
console.log('network-error',error)
}
}
fetchFun()

上述说明fetch请求的异常只有是网络自身的异常会被抛出并捕获,而已连接服务器后的HTTP状态码非200的情况需要手动抛出错误。
补充:与XHR相关的错误处理
javascript
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://example.com/api');
xhr.onerror = function() {
console.error('请求发生错误');
};
xhr.send();
断网、跨域限制、服务器不可达等将触发onerror事件,HTTP状态码错误不会触发onerror
最佳实践:
javascript
function makeRequest(url, method = 'GET', data = null) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject({
status: xhr.status,
statusText: xhr.statusText
});
}
};
xhr.onerror = function() {
reject({
status: 0,
statusText: '网络错误'
});
};
xhr.ontimeout = function() {
reject({
status: 0,
statusText: '请求超时'
});
};
xhr.timeout = 10000;
xhr.send(data);
});
}
// 使用示例
makeRequest('https://example.com/api')
.then(response => console.log(response))
.catch(error => console.error('请求失败:', error));
第二部分
重新抛出错误
思考一个问题:捕获的错误为什么需要重新抛出呢?
javascript
try {
throw new Error('Request failed!')
} catch (error) {
if(error.message.includes('request')){
throw new Error('error about request!',{cause:error})
}
throw error
}
一个原因是:一个错误不断被抛出冒泡到顶层,才可以被某些异常监听日志工具记录
还有一个原因是:抛出一个新错误可以携带更多的错误上下文信息
兼顾用户体验的错误处理的建议
- 除非确实必要,否则不要通知用户发生了错误
某些场景下,接口自动重试机制;非关键特性的js错误发生时;兼容性问题导致需要进行JavaScript的渐进增强与优雅降级
- 提供优质的错误信息
- 自然语言,非技术术语(类型错误)
- 具体(如手机号需要11位的数字)
- 可操作性、可解决问题类的(需要刷新页面,需要联系客服等)
- 与用户体验相关情绪、情感的提示语与配图
- 简短
向开发者展示错误的建议
- 开发者需要技术性的细节
- 提供尽可能多的上下文
- 在服务端应该记录客户端的错误(比如使用网页APM工具,如Sentry)
关于吞掉错误
除了极少数情况(确保是正常可预期的错误),大部分情况需要处理错误或继续抛出错误。
javascript
// ❌ bad case
try {
throw new Error('error')
} catch (error) {
// FIXME
}
// ✅ good case
try {
throw new Error('error')
} catch (error) {
xxx.log(error) // 必要时错误上报
console.log(error) // to devs
alert(error.message) // to users 必要时的用户友好提示
throw new Error('XXX') // 必要时抛出新错误
xxx // 必要时重试或其他业务逻辑处理
}
参考: