JavaScript 错误处理的一种整理与实践

常见的错误来源:

  • 网络请求失败
  • 用户输入非预期的值
  • 接口的异常响应
  • 外部服务的异常

本次分享有什么:

第一部分:核心概念与语法

  1. Try/Catch
  2. 不同类型的错误
  3. 如何创建一个自定义的错误类
  4. 如何在同步和异步代码中处理错误
  5. 与FetchAPI相关的错误处理

第二部分:最佳实践

  1. 什么时候向用户展示错误
  2. 如何展示错误
  3. 如何实现合适的错误日志输出
  4. 如何避免一些常见错误

第一部分

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种错误类型

最常见的错误类型是:

  • RangeError 范围错误

创建自定义错误类型

首先可以考虑下,为什么需要创建自定义错误类型?创建自定义类型的原因是什么?先看完下方的代码,我们再来思考这个问题:

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 是一个自定义的支付场景的错误,携带了更多的上下文信息,可以进行更准确的错误处理和友好提示。在业界主要的应用场景有:

  1. 领域特定错误处理
    1. 为特定业务逻辑创建专门的错误类型(如 **<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>**
  2. 增强错误信息
    1. 添加额外的上下文信息(如请求ID、用户ID、业务状态码),示例:<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">new DatabaseError('Connection failed', { host, port })</font>
  3. 错误分类处理
    1. 通过 **<font style="color:rgb(64, 64, 64);background-color:rgb(236, 236, 236);">instanceof</font>** 进行错误类型判断
    2. 实现精细化的错误处理逻辑
  4. API库设计
    1. 公开明确的错误类型供使用者捕获处理
    2. 提高代码的可维护性和可预测性
  5. 错误监控与日志相关
    1. 便于错误分类统计
    2. 实现更精细的错误上报

实际实践案例:

  • 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
  }

一个原因是:一个错误不断被抛出冒泡到顶层,才可以被某些异常监听日志工具记录

还有一个原因是:抛出一个新错误可以携带更多的错误上下文信息

兼顾用户体验的错误处理的建议

  1. 除非确实必要,否则不要通知用户发生了错误

某些场景下,接口自动重试机制;非关键特性的js错误发生时;兼容性问题导致需要进行JavaScript的渐进增强与优雅降级

  1. 提供优质的错误信息
    1. 自然语言,非技术术语(类型错误)
    2. 具体(如手机号需要11位的数字)
    3. 可操作性、可解决问题类的(需要刷新页面,需要联系客服等)
    4. 与用户体验相关情绪、情感的提示语与配图
    5. 简短

向开发者展示错误的建议

  1. 开发者需要技术性的细节
  2. 提供尽可能多的上下文
  3. 在服务端应该记录客户端的错误(比如使用网页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 // 必要时重试或其他业务逻辑处理
}

参考:

  1. async/await 必须使用 try/catch 吗?-juejin
  2. JavaScript错误处理最佳实践_哔哩哔哩_bilibili
相关推荐
你听得到111 小时前
排查 App 问题:别只盯着报错,把前后发生的事情串起来
android·前端·flutter
lllsure1 小时前
Vue&React Router
前端·vue.js·react.js
weixin_431600441 小时前
为什么 Agent REPL 要上 Ink:好处、用法与内部设计
前端·学习·ai·agent·ai编程
IT_陈寒2 小时前
Java 8的stream让我debug了一整天,气笑了
前端·人工智能·后端
sugar__salt2 小时前
深入理解 React useContext:跨层级组件通信的核心利器
前端·javascript·react.js·前端框架·框架
小黑技术栈2 小时前
web前端基础到入门——14day
前端·数据库·oracle
谷哥的小弟2 小时前
TypeScript类型断言
前端·javascript·typescript
智塑未来2 小时前
鸿蒙系统小红书隐私保护——应用锁开启指南
服务器·前端·harmonyos
东风破_11 小时前
React 受控组件与非受控组件:从一个输入框讲清表单数据流
前端