异步编程是JavaScript绕不开的话题。无论是请求接口、读取文件还是设置定时器,都离不开异步操作。在回调函数时代,嵌套层次一多代码就变成了难以阅读的“回调地狱”,后来Promise用链式调用缓解了这个问题,但真正让异步代码写起来像同步代码一样的,是ES2017引入的async和await。本文将系统讲解async/await是什么、它们在底层是如何工作的,以及实际使用中的技巧和坑点。

一、async/await到底是什么
async是一个用来修饰函数的关键字,被它修饰的函数称为async函数,例如async function getData() {}。await则只能出现在async函数内部(或在模块顶层,即顶层await),它后面通常跟一个Promise对象,表示“等待这个Promise出结果后再继续往下执行”。
先明确一点:async/await并不是全新的异步机制,它完全是建立在Promise之上的语法糖。也就是说,浏览器引擎在背后仍然是靠Promise和事件循环来驱动异步流程的,async/await只是让开发者可以用一种更接近同步代码的风格来组织异步逻辑。
看一个最简单的例子,体会一下两种写法的差异:
// Promise 链式写法
function getUser() {
return fetch('/api/user')
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err));
}
// async/await 写法
async function getUser() {
try {
const res = await fetch('/api/user');
const data = await res.json();
console.log(data);
} catch (err) {
console.error(err);
}
}可以明显看出,async/await版本没有.then和.catch的链式嵌套,逻辑从上往下线性展开,可读性大幅提升。尤其是当多个步骤存在依赖关系时,这种优势更加明显。
二、async函数的返回值机制
很多初学者容易忽略一个关键点:async函数的返回值永远是一个Promise对象。无论你在函数体内return什么,外部拿到的都是Promise。return一个普通值,这个值会被自动包装成已 fulfilled 状态的Promise;return一个Promise,则外部直接等待这个Promise的结果。
async function foo() {
return 1;
}
foo().then(v => console.log(v)); // 1,等价于 return Promise.resolve(1)
async function bar() {
return Promise.resolve(2);
}
bar().then(v => console.log(v)); // 2同样地,如果async函数内部抛出了异常,返回的Promise会进入rejected状态,异常对象会作为拒绝原因传递出去:
async function fail() {
throw new Error('出错了');
}
fail().catch(e => console.log(e.message)); // 出错了
// 等价于
function fail2() {
return Promise.reject(new Error('出错了'));
}理解了这一点,就能明白为什么async函数可以互相组合调用:外层async函数可以直接await另一个async函数,因为后者返回的本身就是Promise,整个异步调用链天然是衔接起来的。
三、await到底在等什么,执行顺序是怎样的
await后面的表达式会先被求值,然后交给await处理。如果求值结果不是Promise(比如一个数字或字符串),它会被包装成已完成的Promise并立即继续执行;如果是一个Promise,函数就会在这里“暂停”,把控制权交还给调用者,等到Promise状态落定后再恢复执行后面的代码。
这里的“暂停”并不是阻塞线程,而是通过事件循环的微任务机制实现的。当await的Promise落定后,await之后的代码会被放入微任务队列,等当前执行栈清空后才执行。我们通过一个经典例子验证:
async function test() {
console.log('1 - async函数开始');
await Promise.resolve();
console.log('4 - await之后的代码(微任务)');
}
console.log('0 - 脚本开始');
test();
console.log('2 - 调用test之后');
setTimeout(() => console.log('5 - 宏任务'), 0);
Promise.resolve().then(() => console.log('3 - 另一个微任务'));
// 输出顺序:0, 1, 2, 3, 4, 5输出结果表明:test()被调用时同步执行到await那一行为止,然后立刻返回,继续执行外部的同步代码;await之后的代码进入微任务队列排队。掌握这个执行顺序对排查异步bug非常重要,比如为什么console.log打印的顺序和代码书写顺序不一致,往往就是微任务在起作用。
另外要注意,await会串行等待。如果多个异步操作之间没有依赖关系,逐个await会造成不必要的等待,这时候应该配合Promise.all并行执行:
// 串行写法:总耗时约 2 秒
async function serial() {
const a = await delay(1000);
const b = await delay(1000);
}
// 并行写法:总耗时约 1 秒
async function parallel() {
const [a, b] = await Promise.all([delay(1000), delay(1000)]);
}
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}四、错误处理的正确姿势
await表达式遇到rejected的Promise时,会在当前位置抛出异常,因此标准的处理方式是try/catch包裹:
async function loadUser() {
try {
const res = await fetch('/api/user');
if (!res.ok) {
throw new Error('HTTP 状态码异常: ' + res.status);
}
return await res.json();
} catch (err) {
console.error('加载失败:', err);
return null;
}
}除了try/catch,也可以在await后面直接调用.catch(),或者在调用async函数的地方用.catch()捕获,写法上更接近Promise风格,适合给单个异步操作兜底的场景。
还有一个常见的坑:在循环中await要谨慎。for...of循环里await会逐次等待,这是符合预期的;但如果把async函数传给forEach,await是不会被等待的,因为forEach本身不认识Promise,回调里的异步操作会“失控”。需要并发时应该使用Promise.all配合map。
// 错误示范:forEach 不会等待 async 回调
[1, 2, 3].forEach(async id => {
await fetchUser(id); // 不会按顺序等待
});
// 正确的串行方式
for (const id of [1, 2, 3]) {
await fetchUser(id);
}
// 正确的并发方式
await Promise.all([1, 2, 3].map(id => fetchUser(id)));五、总结与实践建议
async/await的本质是Promise的语法糖:async函数保证返回Promise,await负责暂停和恢复函数执行,二者配合让异步代码拥有同步般的阅读体验。使用时记住几条原则:一是错误处理不要遗漏,每个可能失败的await都要有兜底;二是无依赖的异步操作尽量用Promise.all并行化;三是理解微任务执行顺序,避免调试时对输出顺序产生困惑。
在现代JavaScript开发中,无论是浏览器端的fetch请求,还是Node.js中读写文件、查询数据库,async/await都已经成为事实上的标准写法。熟练掌握它的原理与细节,不仅能写出更简洁的代码,也能在面对复杂异步流程时做到心中有数。
async/awaitJavaScript异步Promise修改时间:2026-08-31 03:16:41