如何在 JavaScript 中处理异步操作中的错误?Promise 的 catch 和 async/await 的 try-catch。

各位观众老爷,晚上好!我是你们的老朋友,Bug Killer。今天咱们聊聊 JavaScript 里让人又爱又恨的异步操作,以及如何优雅地抓住那些潜伏在暗处的错误。别怕,咱们一步一个脚印,把这些妖魔鬼怪都给降服了。

异步操作:时间旅行者的烦恼

首先,咱们得明白啥是异步操作。想象一下,你点了外卖,然后就眼巴巴地等着骑手小哥送上门。你不可能啥也不干,就盯着手机屏幕等。你可能会刷刷抖音、看看剧,或者跟朋友聊聊天。这就是异步操作的本质:你发起了一个请求(点外卖),然后继续做其他事情,等请求完成(外卖到了)再来处理结果。

在 JavaScript 里,常见的异步操作包括:

  • 网络请求 (fetch, XMLHttpRequest):从服务器获取数据。
  • 定时器 (setTimeout, setInterval):延迟执行代码。
  • 文件读取 (FileReader):读取本地文件。
  • 用户交互 (事件监听):等待用户点击、输入等操作。

异步操作最大的特点就是非阻塞。它不会让你的程序卡住,而是继续执行后面的代码。但这也会带来一个问题:如果异步操作出了错,你该咋办?

Promise:拯救世界的承诺

Promise 就像是一个承诺,它代表一个异步操作的最终结果。这个承诺有三种状态:

  • Pending (等待中):异步操作正在进行。
  • Fulfilled (已完成):异步操作成功完成,并且有了一个结果值。
  • Rejected (已拒绝):异步操作失败,并且有一个错误原因。

有了 Promise,我们就能更方便地处理异步操作的结果,包括成功和失败的情况。

Promise 的 catch:抓住溜走的错误

Promise 提供了一个 catch 方法,专门用来捕获异步操作中的错误。如果 Promise 的状态变成了 Rejectedcatch 方法就会被调用,并且会收到错误原因。

fetch('https://api.example.com/data') // 假设这个地址不存在
  .then(response => {
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    return response.json();
  })
  .then(data => {
    console.log('数据:', data);
  })
  .catch(error => {
    console.error('出错了:', error);
  });

在这个例子中,如果 fetch 请求失败(比如网络错误、服务器返回 404),或者 response.okfalse,就会抛出一个错误。这个错误会被 catch 方法捕获,并且打印到控制台。

catch 的使用技巧

  • 链式调用catch 可以链式调用在 then 之后,这样可以捕获整个 Promise 链中的错误。

    fetch('https://api.example.com/data')
      .then(response => response.json())
      .then(data => {
        // 处理数据
        throw new Error('故意制造的错误');
      })
      .catch(error => {
        console.error('捕获到错误:', error);
      });
  • 多个 catch:虽然不常见,但你也可以在 Promise 链中添加多个 catch。每个 catch 都会尝试处理错误,如果它能处理,错误就会被消费掉,后面的 catch 就不会被调用。

    fetch('https://api.example.com/data')
      .then(response => response.json())
      .then(data => {
        // 处理数据
        throw new Error('故意制造的错误');
      })
      .catch(TypeError, error => {
          console.error('捕获到 TypeError 错误:', error);
      })
      .catch(error => {
        console.error('捕获到其他错误:', error); // 如果上面的 catch 没有处理,这里会捕获
      });
  • finally:Promise 还有一个 finally 方法,它会在 Promise 完成(FulfilledRejected)后都会被调用。它通常用于清理资源,比如关闭连接、释放内存等。

    fetch('https://api.example.com/data')
      .then(response => response.json())
      .then(data => {
        // 处理数据
      })
      .catch(error => {
        console.error('出错了:', error);
      })
      .finally(() => {
        console.log('请求完成,无论成功还是失败');
      });

async/await:异步操作的语法糖

async/await 是 ES2017 引入的一种更简洁的异步操作语法。它本质上是 Promise 的语法糖,让你可以像写同步代码一样写异步代码。

try-catch:包围你的异步代码

async/await 配合 try-catch 语句可以更方便地处理异步操作中的错误。你可以把异步代码放在 try 块中,如果发生错误,就会被 catch 块捕获。

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new Error('Network response was not ok');
    }
    const data = await response.json();
    console.log('数据:', data);
    return data;
  } catch (error) {
    console.error('出错了:', error);
    // 可以选择在这里重新抛出错误,让调用者处理
    throw error;
    // 或者返回一个默认值
    // return null;
  }
}

async function main() {
    try {
        const data = await fetchData();
        console.log('main 函数接收到的数据:', data);
    } catch (error) {
        console.error('main 函数捕获的错误:', error);
    }
}

main();

在这个例子中,fetchData 函数使用 async/await 发起网络请求。如果请求失败,或者 response.okfalse,就会抛出一个错误。这个错误会被 catch 块捕获,并且打印到控制台。如果 fetchData 内部的 catch 块重新抛出了错误,那么 main 函数的 catch 块也会捕获到这个错误。

try-catch 的使用技巧

  • 只包裹异步代码try 块应该只包含异步代码,避免包裹无关的代码,以免捕获到意料之外的错误。

    async function fetchData() {
      try {
        const response = await fetch('https://api.example.com/data');
        const data = await response.json();
        return data;
      } catch (error) {
        console.error('出错了:', error);
        throw error;
      }
    }
    
    function syncFunction() {
      // 一些同步代码
      if (someCondition) {
        throw new Error('同步代码中的错误');
      }
    }
    
    async function main() {
      try {
        syncFunction(); // 同步代码,不应该放在 fetchData 的 try 块中
        const data = await fetchData();
        console.log('数据:', data);
      } catch (error) {
        console.error('main 函数捕获的错误:', error);
      }
    }
  • 错误处理策略:在 catch 块中,你可以选择:

    • 记录错误:将错误信息记录到日志中,方便后续分析。
    • 重新抛出错误:将错误传递给调用者,让调用者处理。
    • 返回默认值:返回一个默认值,避免程序崩溃。
    • 重试:尝试重新执行异步操作。
    • 显示错误信息:向用户显示错误信息。
  • 嵌套 try-catch:你可以在 try 块中嵌套 try-catch 语句,以便更精细地处理错误。

    async function fetchData() {
      try {
        const response = await fetch('https://api.example.com/data');
        try {
          const data = await response.json();
          return data;
        } catch (jsonError) {
          console.error('JSON 解析错误:', jsonError);
          throw jsonError;
        }
      } catch (networkError) {
        console.error('网络请求错误:', networkError);
        throw networkError;
      }
    }

Promise.all 和 Promise.allSettled

当我们需要同时执行多个异步操作时,可以使用 Promise.allPromise.allSettled

  • Promise.all:接收一个 Promise 数组,当所有 Promise 都成功完成时,返回一个包含所有结果的 Promise。如果其中任何一个 Promise 失败,Promise.all 就会立即失败,并且返回第一个失败的 Promise 的错误原因。

    async function fetchData1() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve('数据 1');
        }, 500);
      });
    }
    
    async function fetchData2() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          reject('数据 2 获取失败');
        }, 1000);
      });
    }
    
    async function main() {
      try {
        const results = await Promise.all([fetchData1(), fetchData2()]);
        console.log('所有数据:', results); // 如果 fetchData2 失败,这里不会执行
      } catch (error) {
        console.error('出错了:', error); // 这里会捕获 fetchData2 的错误
      }
    }
    
    main();
  • Promise.allSettled:也接收一个 Promise 数组,但它会等待所有 Promise 都完成(无论成功还是失败),然后返回一个包含每个 Promise 结果的数组。数组中的每个元素都是一个对象,包含 statusvaluereason 属性。

    async function fetchData1() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve('数据 1');
        }, 500);
      });
    }
    
    async function fetchData2() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          reject('数据 2 获取失败');
        }, 1000);
      });
    }
    
    async function main() {
      const results = await Promise.allSettled([fetchData1(), fetchData2()]);
      console.log('所有结果:', results);
      // results 的结构:
      // [
      //   { status: 'fulfilled', value: '数据 1' },
      //   { status: 'rejected', reason: '数据 2 获取失败' }
      // ]
      const successfulResults = results.filter(result => result.status === 'fulfilled').map(result => result.value);
      const failedResults = results.filter(result => result.status === 'rejected').map(result => result.reason);
      console.log('成功的数据:', successfulResults);
      console.log('失败的原因:', failedResults);
    }
    
    main();

错误处理的最佳实践

  • 尽早处理错误:不要等到程序崩溃才处理错误,应该在错误发生的第一时间就进行处理。
  • 提供有用的错误信息:错误信息应该包含足够的信息,方便你定位问题。
  • 记录错误:将错误信息记录到日志中,方便后续分析。
  • 避免过度捕获:不要捕获所有错误,应该只捕获那些你能处理的错误。
  • 使用合适的错误处理机制:根据不同的场景选择合适的错误处理机制,比如 catchtry-catchPromise.allSettled 等。
  • 考虑用户体验:向用户显示友好的错误信息,避免让用户感到困惑。

错误类型和处理

JavaScript 中常见的错误类型包括:

错误类型 描述 示例
Error 错误基类,所有错误的父类。 throw new Error('Something went wrong');
TypeError 变量或参数不是预期类型时抛出。 let num = 'abc'; num.toUpperCase(); // String.prototype.toUpperCase called on null or undefined
ReferenceError 尝试使用未声明的变量时抛出。 console.log(undeclaredVariable);
SyntaxError 代码中存在语法错误时抛出。 eval('invalid javascript code');
RangeError 数字超出允许的范围时抛出。 let arr = new Array(-1); // Invalid array length
URIError encodeURI()decodeURI() 使用了无效的 URI 参数时抛出。 decodeURI('%'); // URI malformed
EvalError eval() 函数使用不当(已废弃,不再抛出)。 N/A
AggregateError Promise.allSettled() 在所有 Promise 都被拒绝时抛出。 See Promise.allSettled() example above.

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

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    return data;
  } catch (error) {
    if (error instanceof TypeError) {
      console.error('类型错误:', error);
      // 处理类型错误
    } else if (error instanceof ReferenceError) {
      console.error('引用错误:', error);
      // 处理引用错误
    } else {
      console.error('其他错误:', error);
      // 处理其他错误
    }
    throw error; // 重新抛出错误
  }
}

总结

异步操作是 JavaScript 中不可避免的一部分,而错误处理则是保证程序健壮性的关键。无论是使用 Promise 的 catch 方法,还是 async/awaittry-catch 语句,都要记住尽早处理错误、提供有用的错误信息、记录错误、避免过度捕获,并且选择合适的错误处理机制。

希望今天的讲座能帮助你更好地理解 JavaScript 异步操作中的错误处理。记住,Bug Killer 与你同在,祝你早日成为 Bug Free 的大神! 下次再见!

发表回复

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