JavaScript异步编程教程:Promise、async/await与错误处理实践

JavaScript 是单线程语言,但前端开发里经常要处理异步任务,比如请求接口、读取文件、定时器、用户事件、动画回调等。异步编程写得清楚,页面交互会更稳定;写得混乱,就容易出现回调嵌套、错误丢失、状态错乱等问题。

现代 JavaScript 主要通过 Promiseasync/await 处理异步逻辑。本文从异步任务的基本概念讲起,再介绍 Promise 链式调用、async/await 写法和错误处理实践。

为什么需要异步

浏览器执行 JavaScript 时,如果某个任务耗时很长,页面就可能卡住。网络请求、定时等待、文件读取这些操作不能让主线程一直阻塞,所以它们通常以异步方式执行。

异步的意思不是任务更快完成,而是先把耗时任务交出去,等结果回来后再继续处理。这样页面可以继续响应用户操作。

回调函数问题

早期异步代码常用回调函数。简单场景没问题,但多个异步任务依赖彼此时,很容易形成多层嵌套。

getUser((user) => {
  getOrders(user.id, (orders) => {
    getDetail(orders[0].id, (detail) => {
      console.log(detail);
    });
  });
});

这种写法被称为回调地狱。代码越深,错误处理和流程控制越难维护。Promise 的出现就是为了解决这类问题。

JavaScript异步编程教程配图:Promise 与 async await 流程
异步编程的目标,是让耗时任务不阻塞页面,同时让代码流程保持清晰可维护。

Promise 基础

Promise 表示一个未来会完成或失败的异步结果。它有三种状态:等待中、已成功、已失败。状态一旦改变,就不会再回退。

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('成功结果');
  }, 1000);
});

成功时调用 resolve,失败时调用 reject。实际项目里,我们更多是使用接口请求或工具函数返回的 Promise,而不是每次手动 new。

then 和 catch

Promise 可以通过 then 获取成功结果,通过 catch 捕获错误。

fetch('/api/user')
  .then((response) => response.json())
  .then((data) => {
    console.log(data);
  })
  .catch((error) => {
    console.error('请求失败', error);
  });

链式调用可以避免多层回调嵌套。每个 then 返回的结果,会传给下一个 then

Promise.all

如果多个异步任务可以并行执行,并且都完成后再继续,可以使用 Promise.all

const [user, orders] = await Promise.all([
  fetchUser(),
  fetchOrders()
]);

需要注意,只要其中一个 Promise 失败,Promise.all 就会整体失败。如果希望每个任务独立返回结果,可以考虑 Promise.allSettled

async 和 await

async/await 是 Promise 的语法糖,它让异步代码看起来更像同步流程。函数前加 async 后,内部可以使用 await 等待 Promise 结果。

async function loadUser() {
  const response = await fetch('/api/user');
  const data = await response.json();
  console.log(data);
}

await 只会暂停当前 async 函数的后续执行,不会阻塞整个浏览器主线程。页面仍然可以继续响应其他事件。

错误处理

在 async 函数里,常用 try...catch 捕获异步错误。

async function loadUser() {
  try {
    const response = await fetch('/api/user');
    const data = await response.json();
    return data;
  } catch (error) {
    console.error('加载用户失败', error);
    throw error;
  }
}

捕获错误后,要根据业务决定是否继续抛出。如果上层还需要统一处理,就应该重新抛出;如果当前函数已经处理完错误,可以返回兜底结果。

接口状态检查

fetch 在 HTTP 404 或 500 时不会自动进入 catch,只有网络错误才会 reject。因此要手动检查 response.ok

async function request(url) {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error(`HTTP error: ${response.status}`);
  }

  return response.json();
}

这是很多新手容易忽略的点。接口返回错误状态码时,也应该走统一错误处理流程。

避免串行等待

如果两个异步任务互不依赖,不要写成串行 await。串行会让总耗时变长。

const userPromise = fetchUser();
const configPromise = fetchConfig();

const user = await userPromise;
const config = await configPromise;

更简洁的写法是使用 Promise.all。只有后一个任务依赖前一个结果时,才需要串行等待。

finally 清理状态

无论成功还是失败,都需要执行的清理逻辑,可以放在 finally 里。比如关闭 loading、恢复按钮状态。

async function submit() {
  loading = true;
  try {
    await saveForm();
  } finally {
    loading = false;
  }
}

这样可以避免请求失败后 loading 一直不消失。表单提交、弹窗确认、上传文件等场景都很常用。

常见错误

第一种错误是忘记写 await,拿到的不是结果而是 Promise。第二种错误是在循环里无脑 await,导致本可并行的任务变成串行。第三种错误是 catch 后吞掉错误,上层完全不知道失败。第四种错误是没有处理 loading 和重复提交状态。

还有一种常见问题是把 async 函数传给事件处理器后,内部错误没有被妥善捕获。涉及用户操作的异步逻辑,最好有明确的错误提示。

实践建议

写异步代码时,先判断任务之间是否有依赖。互不依赖就并行,存在依赖才串行。请求函数要统一处理 HTTP 状态和 JSON 解析。页面交互要用 try、catch、finally 管理成功、失败和清理状态。

Promise 和 async/await 不是两套完全不同的东西,async/await 只是让 Promise 写起来更顺。理解 Promise 的状态和错误传播,再使用 async/await,异步代码就会清晰很多。

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容