异步编程一直是JavaScript中绕不开的话题。从回调函数到Promise,再到Generator,JavaScript的异步方案不断演进,而ES6之后引入的ES2017标准中的async函数(俗称ES6 async函数的延伸),可以说是目前最优雅的异步解决方案。它让你用同步的写法处理异步逻辑,代码可读性大幅提升。本文将从基本语法、执行原理、错误处理到常见误区,把async函数的知识点一次性讲透。

一、async函数的基本语法与返回值本质
async函数是在普通函数前加上async关键字修饰的函数,它的核心特点是:无论函数体内return什么,返回值永远是一个Promise对象。这是理解async函数一切行为的基础。
具体规则是:如果return一个普通值,这个值会被Promise.resolve()包装成resolved状态的Promise;如果return一个Promise,则直接返回这个Promise的状态;如果函数内部抛出异常,返回的Promise会变成rejected状态。看下面的例子:
async function foo() {
return 1;
}
foo().then(val => console.log(val)); // 1
// 等价于
function bar() {
return Promise.resolve(1);
}
async function baz() {
throw new Error('出错了');
}
baz().catch(err => console.log(err.message)); // 出错了很多初学者会误以为return await somePromise和直接return somePromise有区别。在绝大多数场景下两者效果相同,但在try/catch捕获错误的场景下,return await能把异步操作的异常留在当前函数的作用域内被catch捕获,而直接return则会让异常继续向上抛出。这是一个容易被忽略的细节。
二、await的执行顺序与错误处理机制
await后面通常跟一个Promise对象,它会让async函数暂停执行,等待Promise的状态确定后再继续往下执行。如果await的不是Promise,会先被Promise.resolve()转换。await返回的结果是Promise resolved时携带的值。
理解执行顺序的关键在于:async函数遇到await时会暂停,把控制权交还给调用者,也就是让出主线程,后续代码进入微任务队列。来看一个经典例子:
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
async1();
console.log('script end');
// 输出顺序:script start -> async1 start -> async2 -> script end -> async1 end错误处理方面,如果await后面的Promise被reject,await表达式会抛出错误,整个async函数中断执行。因此务必要做好错误捕获,常用方式有两种:一种是外层包裹try/catch,另一种是给Promise本身链一个catch,让失败变成一个正常值继续处理:
// 方式一:try/catch
async function load() {
try {
const res = await fetch('/api/data');
const data = await res.json();
return data;
} catch (e) {
console.error('请求失败:', e);
return null;
}
}
// 方式二:await接catch,不中断后续逻辑
async function load2() {
const res = await fetch('/api/data').catch(e => null);
if (!res) return;
return res.json();
}三、常见误区与坑点解析
第一个高频误区是在循环中使用await。如果你在for循环里逐个await,多个异步任务会串行执行,总耗时等于每个任务耗时之和;而如果想在forEach里使用await,很遗憾这根本不生效,因为forEach的回调是普通函数,不会等待异步完成。正确的并行写法是配合Promise.all:
// 串行写法,总耗时约3秒
async function serial() {
for (const url of ['/a', '/b', '/c']) {
await fetch(url);
}
}
// 并行写法,总耗时约1秒
async function parallel() {
const tasks = ['/a', '/b', '/c'].map(url => fetch(url));
await Promise.all(tasks);
}
// forEach中使用await无效,不会等待
['/a', '/b'].forEach(async url => {
await fetch(url); // forEach本身已返回,此处不会被等待
});第二个常见问题是忘记await导致拿到的是Promise对象而不是值,例如写了const data = getData()而getData是async函数,那么data是一个Promise,调用data.xxx会得到undefined。遇到“属性为undefined”的报错时,优先检查是否漏写了await。
第三个坑是async函数中的顶层异常。由于async函数返回Promise,函数内部的同步错误也不会抛出到调用处的try/catch,而是走Promise的rejected通道。所以调用async函数时,要么用await配合try/catch,要么用.catch()兜底,否则会出现unhandled promise rejection警告。
四、async函数与Generator、Promise的关系
从语法上说,async函数可以看作Generator函数的语法糖。Generator用function*声明,配合yield和执行器(如co库)才能实现异步流程控制;而async函数把星号换成了async,把yield换成了await,并且内置了执行器,调用时自动逐步执行到底,不需要额外引入执行器。
对比Promise链式调用,async函数的优势在于消灭了层层嵌套的then回调,用线性的代码结构表达异步流程,错误处理也能统一用try/catch完成。三者可以这样理解:Promise是基础设施,Generator提供了可暂停函数的能力,async函数在两者之上提供了最接近同步思维的开发体验。
需要补充的是,await只能出现在async函数内部这条限制在较新的运行环境中已经放宽,顶级await可以在ES模块顶层直接使用,省去了包裹一层async函数的麻烦。不过在实际项目中,是否可用还要看目标运行环境的支持情况,老项目建议还是通过立即执行函数包裹的方式兼容。
掌握async函数的关键在于抓住两点:返回值永远是Promise,await会暂停并等待状态确定。把执行顺序和错误处理这两个核心机制吃透,日常开发中大部分异步问题都能迎刃而解。
异步编程一直是JavaScript中绕不开的话题。从回调函数到Promise,再到Generator,JavaScript的异步方案不断演进,而ES6之后引入的ES2017标准中的async函数(俗称ES6 async函数的延伸),可以说是目前最优雅的异步解决方案。它让你用同步的写法处理异步逻辑,代码可读性大幅提升。本文将从基本语法、执行原理、错误处理到常见误区,把async函数的知识点一次性讲透。

一、async函数的基本语法与返回值本质
async函数是在普通函数前加上async关键字修饰的函数,它的核心特点是:无论函数体内return什么,返回值永远是一个Promise对象。这是理解async函数一切行为的基础。
具体规则是:如果return一个普通值,这个值会被Promise.resolve()包装成resolved状态的Promise;如果return一个Promise,则直接返回这个Promise的状态;如果函数内部抛出异常,返回的Promise会变成rejected状态。看下面的例子:
async function foo() {
return 1;
}
foo().then(val => console.log(val)); // 1
// 等价于
function bar() {
return Promise.resolve(1);
}
async function baz() {
throw new Error('出错了');
}
baz().catch(err => console.log(err.message)); // 出错了很多初学者会误以为return await somePromise和直接return somePromise有区别。在绝大多数场景下两者效果相同,但在try/catch捕获错误的场景下,return await能把异步操作的异常留在当前函数的作用域内被catch捕获,而直接return则会让异常继续向上抛出。这是一个容易被忽略的细节。
二、await的执行顺序与错误处理机制
await后面通常跟一个Promise对象,它会让async函数暂停执行,等待Promise的状态确定后再继续往下执行。如果await的不是Promise,会先被Promise.resolve()转换。await返回的结果是Promise resolved时携带的值。
理解执行顺序的关键在于:async函数遇到await时会暂停,把控制权交还给调用者,也就是让出主线程,后续代码进入微任务队列。来看一个经典例子:
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
async1();
console.log('script end');
// 输出顺序:script start -> async1 start -> async2 -> script end -> async1 end错误处理方面,如果await后面的Promise被reject,await表达式会抛出错误,整个async函数中断执行。因此务必要做好错误捕获,常用方式有两种:一种是外层包裹try/catch,另一种是给Promise本身链一个catch,让失败变成一个正常值继续处理:
// 方式一:try/catch
async function load() {
try {
const res = await fetch('/api/data');
const data = await res.json();
return data;
} catch (e) {
console.error('请求失败:', e);
return null;
}
}
// 方式二:await接catch,不中断后续逻辑
async function load2() {
const res = await fetch('/api/data').catch(e => null);
if (!res) return;
return res.json();
}三、常见误区与坑点解析
第一个高频误区是在循环中使用await。如果你在for循环里逐个await,多个异步任务会串行执行,总耗时等于每个任务耗时之和;而如果想在forEach里使用await,很遗憾这根本不生效,因为forEach的回调是普通函数,不会等待异步完成。正确的并行写法是配合Promise.all:
// 串行写法,总耗时约3秒
async function serial() {
for (const url of ['/a', '/b', '/c']) {
await fetch(url);
}
}
// 并行写法,总耗时约1秒
async function parallel() {
const tasks = ['/a', '/b', '/c'].map(url => fetch(url));
await Promise.all(tasks);
}
// forEach中使用await无效,不会等待
['/a', '/b'].forEach(async url => {
await fetch(url); // forEach本身已返回,此处不会被等待
});第二个常见问题是忘记await导致拿到的是Promise对象而不是值,例如写了const data = getData()而getData是async函数,那么data是一个Promise,调用data.xxx会得到undefined。遇到“属性为undefined”的报错时,优先检查是否漏写了await。
第三个坑是async函数中的顶层异常。由于async函数返回Promise,函数内部的同步错误也不会抛出到调用处的try/catch,而是走Promise的rejected通道。所以调用async函数时,要么用await配合try/catch,要么用.catch()兜底,否则会出现unhandled promise rejection警告。
四、async函数与Generator、Promise的关系
从语法上说,async函数可以看作Generator函数的语法糖。Generator用function*声明,配合yield和执行器(如co库)才能实现异步流程控制;而async函数把星号换成了async,把yield换成了await,并且内置了执行器,调用时自动逐步执行到底,不需要额外引入执行器。
对比Promise链式调用,async函数的优势在于消灭了层层嵌套的then回调,用线性的代码结构表达异步流程,错误处理也能统一用try/catch完成。三者可以这样理解:Promise是基础设施,Generator提供了可暂停函数的能力,async函数在两者之上提供了最接近同步思维的开发体验。
需要补充的是,await只能出现在async函数内部这条限制在较新的运行环境中已经放宽,顶级await可以在ES模块顶层直接使用,省去了包裹一层async函数的麻烦。不过在实际项目中,是否可用还要看目标运行环境的支持情况,老项目建议还是通过立即执行函数包裹的方式兼容。
掌握async函数的关键在于抓住两点:返回值永远是Promise,await会暂停并等待状态确定。把执行顺序和错误处理这两个核心机制吃透,日常开发中大部分异步问题都能迎刃而解。