JS `try-catch` 在 `async/await` 中的错误处理最佳实践

各位观众老爷们,大家好!今天咱们来聊聊 JavaScript 里 try...catchasync/await 这一对儿的爱恨情仇,以及如何正确地用它们来处理异步代码中的错误。这俩家伙配合好了,能让你的代码健壮得像个钢铁侠;配合不好,那你的程序就可能像个纸糊的房子,风一吹就倒。

开场白:异步世界的错误处理难题

在同步代码的世界里,错误处理相对简单,try...catch 就像一个坚实的盾牌,挡住可能抛出的异常。但是,当代码进入异步的领域,尤其是在 async/await 的加持下,事情就开始变得微妙起来。

为什么呢?因为 async/await 本质上是 Promise 的语法糖。Promise 内部抛出的错误,如果没被正确处理,可能会被淹没在异步的洪流中,让你 debug 的时候挠破头皮都找不到原因。

第一幕:try...catch 的基本用法回顾

先来复习一下 try...catch 的基本姿势:

try {
  // 可能会抛出异常的代码
  console.log("开始执行...");
  //throw new Error("故意抛出一个错误"); // 模拟错误
  console.log("执行成功!"); // 如果有错误,这句不会执行
} catch (error) {
  // 捕获并处理异常
  console.error("发生错误啦:", error);
} finally {
  // 无论是否发生错误,都会执行的代码 (可选)
  console.log("无论如何我都会执行!");
}
  • try 块: 包含你认为可能会抛出异常的代码。
  • catch 块: 如果 try 块中抛出了异常,就会被 catch 块捕获,并执行其中的代码。error 参数包含了错误的信息。
  • finally 块: 无论 try 块是否抛出异常,finally 块中的代码都会执行。通常用于清理资源,比如关闭文件、释放连接等。

第二幕:async/await 中的 try...catch 大法

现在,我们把 try...catch 带入 async/await 的世界。

async function fetchData() {
  try {
    console.log("开始获取数据...");
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    const data = await response.json();
    console.log("数据获取成功:", data);
    return data; // 返回数据
  } catch (error) {
    console.error("获取数据失败:", error);
    // 错误处理的几种方式:
    // 1. 重新抛出错误,让调用者处理
    // throw error;
    // 2. 返回一个默认值或错误对象
    // return { error: error.message };
    // 3. 静默处理,记录日志
    // console.log("已经记录错误日志");
    throw error; // 建议:重新抛出,由上层函数决定如何处理
  } finally {
    console.log("数据获取流程结束!");
  }
}

async function processData() {
  try {
    const data = await fetchData();
    console.log("数据处理中...", data);
    // ... 对数据进行处理
  } catch (error) {
    console.error("处理数据失败:", error);
  }
}

processData(); // 调用入口函数

这段代码演示了在 async 函数中使用 try...catch 来捕获 await 操作可能抛出的错误。

  • 关键点: try...catch 必须包裹 await 表达式。 只有这样,才能捕获 await 期间发生的错误,比如网络请求失败、JSON 解析错误等等。
  • 错误处理策略:catch 块中,你可以选择多种错误处理策略:
    • 重新抛出错误 (throw error;): 将错误传递给调用者,让上层函数来处理。这是一种常见的做法,可以将错误处理的责任向上转移,使得代码更加模块化。
    • 返回一个默认值或错误对象 (return { error: error.message };): 如果错误不影响程序的继续运行,可以返回一个默认值,或者一个包含错误信息的对象。
    • 静默处理,记录日志 (console.log("已经记录错误日志");): 这种方式适用于一些不重要的错误,可以记录日志,方便后续分析。但是要谨慎使用,避免掩盖潜在的问题。

第三幕:更高级的错误处理技巧

光会用 try...catch 还不够,我们还要掌握一些更高级的技巧,让错误处理更加优雅和高效。

  1. 多个 await 的情况:

    如果你的 async 函数中包含多个 await 表达式,你需要仔细考虑 try...catch 的范围。

    async function multipleAwaits() {
      try {
        const result1 = await someAsyncFunction1();
        console.log("result1:", result1);
        const result2 = await someAsyncFunction2(result1);
        console.log("result2:", result2);
        const result3 = await someAsyncFunction3(result2);
        console.log("result3:", result3);
        return result3;
      } catch (error) {
        console.error("发生错误:", error);
        // 错误处理...
        throw error; // 重新抛出
      }
    }

    在这个例子中,try...catch 包裹了所有的 await 表达式。这意味着,任何一个 await 失败,都会被 catch 块捕获。如果只想捕获特定 await 的错误,可以单独包裹:

    async function multipleAwaitsIndividual() {
      try {
        const result1 = await someAsyncFunction1();
        console.log("result1:", result1);
      } catch (error) {
        console.error("someAsyncFunction1 失败:", error);
        // 处理 result1 相关的错误
        throw error;
      }
    
      try {
        const result2 = await someAsyncFunction2(result1);
        console.log("result2:", result2);
      } catch (error) {
        console.error("someAsyncFunction2 失败:", error);
        // 处理 result2 相关的错误
        throw error;
      }
    
      try {
        const result3 = await someAsyncFunction3(result2);
        console.log("result3:", result3);
        return result3;
      } catch (error) {
        console.error("someAsyncFunction3 失败:", error);
        // 处理 result3 相关的错误
        throw error;
      }
    }

    选择哪种方式取决于你的具体需求。如果所有 await 失败都需要进行相同的处理,那么包裹所有 await 是最简洁的方式。如果每个 await 失败都需要进行不同的处理,那么单独包裹每个 await 更加灵活。

  2. 错误类型判断:

    catch 块中,你可以根据错误类型来进行不同的处理。

    async function handleErrorsByType() {
      try {
        // ... 一些异步操作
      } catch (error) {
        if (error instanceof TypeError) {
          console.error("类型错误:", error);
          // 处理类型错误
        } else if (error instanceof ReferenceError) {
          console.error("引用错误:", error);
          // 处理引用错误
        } else {
          console.error("未知错误:", error);
          // 处理其他错误
        }
        throw error;
      }
    }

    这可以让你更精确地处理不同类型的错误,提供更友好的错误提示。

  3. 自定义错误类:

    为了更好地组织和管理错误,你可以创建自定义的错误类。

    class CustomError extends Error {
      constructor(message, code) {
        super(message);
        this.name = "CustomError";
        this.code = code;
      }
    }
    
    async function useCustomError() {
      try {
        // ... 一些异步操作
        throw new CustomError("自定义错误信息", 500);
      } catch (error) {
        if (error instanceof CustomError) {
          console.error("自定义错误:", error.message, error.code);
          // 处理自定义错误
        } else {
          console.error("其他错误:", error);
          // 处理其他错误
        }
        throw error;
      }
    }

    自定义错误类可以包含更多的错误信息,比如错误码、错误类型等,方便你进行更详细的错误分析和处理。

  4. 使用 Promise.all 的错误处理:

    当使用 Promise.all 并发执行多个 Promise 时,如果其中一个 Promise 失败,整个 Promise.all 就会 reject。 你需要使用 try...catch 来捕获这个 reject。

    async function handlePromiseAllErrors() {
      try {
        const [result1, result2, result3] = await Promise.all([
          someAsyncFunction1(),
          someAsyncFunction2(),
          someAsyncFunction3(),
        ]);
        console.log("所有 Promise 都成功:", result1, result2, result3);
      } catch (error) {
        console.error("至少一个 Promise 失败:", error);
        // 处理错误
        throw error;
      }
    }

    注意:Promise.all 在遇到第一个 reject 之后,就不会再等待其他的 Promise 完成。 如果你需要等待所有的 Promise 完成,并收集所有的错误,可以考虑使用 Promise.allSettled

  5. 错误处理中间件 (Node.js):

    在 Node.js 的 Web 应用中,可以使用错误处理中间件来集中处理错误。

    // 错误处理中间件
    function errorHandler(err, req, res, next) {
      console.error("服务器错误:", err.stack);
      res.status(500).send('Something broke!');
    }
    
    // 在 Express 应用中使用
    const express = require('express');
    const app = express();
    
    app.get('/', async (req, res) => {
      try {
        // ... 一些异步操作
        const data = await fetchData();
        res.send(data);
      } catch (error) {
        next(error); // 将错误传递给错误处理中间件
      }
    });
    
    app.use(errorHandler); // 注册错误处理中间件

    错误处理中间件可以捕获所有未被处理的错误,并进行统一的处理,比如记录日志、返回错误响应等。

第四幕:最佳实践总结

为了让你更好地掌握 try...catchasync/await 中的使用,我总结了一些最佳实践:

实践 说明 示例
始终包裹 await 表达式 try...catch 必须包裹 await 表达式,才能捕获异步操作可能抛出的错误。 try { const result = await someAsyncFunction(); } catch (error) { ... }
选择合适的错误处理策略 根据具体情况选择合适的错误处理策略,比如重新抛出错误、返回默认值、静默处理等。 catch (error) { throw error; }catch (error) { return { error: error.message }; }
根据错误类型进行处理 catch 块中,可以根据错误类型来进行不同的处理,提供更友好的错误提示。 catch (error) { if (error instanceof TypeError) { ... } }
使用自定义错误类 创建自定义的错误类,可以包含更多的错误信息,方便你进行更详细的错误分析和处理。 class CustomError extends Error { ... }
处理 Promise.all 的错误 当使用 Promise.all 并发执行多个 Promise 时,需要使用 try...catch 来捕获可能发生的 reject。 try { await Promise.all([...]); } catch (error) { ... }
使用错误处理中间件 (Node.js) 在 Node.js 的 Web 应用中,可以使用错误处理中间件来集中处理错误。 app.use(errorHandler);
避免过度使用 try...catch 不要滥用 try...catch,只在必要的地方使用。 过多的 try...catch 会使代码变得臃肿和难以维护。 考虑使用条件判断来避免一些简单的错误,而不是使用 try...catch
编写单元测试 编写单元测试来验证你的错误处理逻辑是否正确。 使用 Jest 或 Mocha 等测试框架来编写单元测试。
记录详细的错误日志 记录详细的错误日志,包括错误信息、发生时间、调用栈等,方便后续分析和调试。 使用 Winston 或 Morgan 等日志库来记录错误日志。
考虑使用 Promise.allSettled 如果需要等待所有 promise 执行完成,不关心成功与否,可以使用 Promise.allSettled 来收集所有 promise 的结果,包括成功和失败。 从而避免因为一个 promise 的失败而导致整个流程中断。 const results = await Promise.allSettled([promise1, promise2, promise3]); results.forEach(result => { if(result.status === 'rejected') { console.error('Promise rejected:', result.reason); } });

第五幕:总结与展望

今天我们深入探讨了 try...catchasync/await 中的使用。掌握了这些技巧,你就可以编写出更加健壮、可靠的异步代码。记住,错误处理是软件开发中至关重要的一环,不要掉以轻心。

希望今天的分享对你有所帮助。 感谢各位的观看,下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注