技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

setTimeout、Promise、async/await 的区别

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/57-setTimeout、Promise、Async Await 的区别
本文目录9 个章节
  1. 一、三者不是同一层面的东西
  2. 二、setTimeout:至少等待一段时间后执行任务
  3. 三、Promise:用微任务连接异步结果
  4. 四、async/await:Promise 的语法层
  5. 五、三者放在一起时的执行顺序
  6. 六、错误处理方式
  7. 七、顺序执行和并发执行
  8. 八、如何选择
  9. 参考资料

setTimeout、Promise、async/await 的区别

Category(分类): JavaScript Status: 已整理(2026)

原文只有题目和来源链接:setTimeout、Promise、Async Await 的区别。本文保留这个比较主题,并补充三者的执行模型、常见输出、错误处理和并发写法。

一、三者不是同一层面的东西

机制主要作用典型结果
setTimeout向宿主计时器注册一个稍后执行的回调返回计时器句柄,回调进入任务队列
Promise表示异步操作的最终结果,并组合成功/失败链返回 Promise 对象,处理器进入微任务队列
async/await以更接近同步代码的语法消费 Promiseasync 函数始终返回 Promise

setTimeout 是宿主提供的计时器 API;Promise 是 ECMAScript 的语言抽象;async/await 是建立在 Promise 之上的语言语法。它们经常一起使用,但不能互相替换。

二、setTimeout:至少等待一段时间后执行任务

console.log('start')

setTimeout(() => {
  console.log('timer')
}, 0)

console.log('end')

输出一定先是:

start
end

timer 会在当前同步代码执行完、事件循环有机会处理该任务时运行。0 不是“立刻执行”,也不是严格的 0 毫秒保证;延迟时间只是一个不早于该时间的调度条件,实际还受当前任务、浏览器嵌套计时器规则、后台页面节流和 Node.js 事件循环影响。

const startedAt = Date.now()

setTimeout(() => {
  console.log(`至少等待约 ${Date.now() - startedAt} 毫秒`)
}, 10)

计时器回调属于宿主任务(task/macrotask 的常用说法)。任务执行结束后,宿主通常会处理当前批次的微任务,然后再继续下一个任务;浏览器还会在合适时机进行渲染。

三、Promise:用微任务连接异步结果

Promise 构造器的 executor 是同步调用的:

console.log('before')

const promise = new Promise(resolve => {
  console.log('executor')
  setTimeout(() => resolve('resolved'), 0)
})

promise.then(value => console.log(value))
console.log('after')

输出顺序是:

before
executor
after
resolved

Promise 的 executor 同步执行,但 setTimeout 是异步的;then 的处理函数也不会同步执行。即使 Promise 已经 fulfilled,处理器仍会作为 Promise reaction 进入微任务队列:

const order = []

Promise.resolve().then(() => order.push('promise'))
order.push('sync')

console.log(order) // ['sync']

setTimeout(() => {
  console.log(order) // ['sync', 'promise']
}, 0)

在浏览器中,当前任务结束后通常先清空微任务,再考虑下一个任务和渲染;不要把“微任务先于计时器”理解成所有宿主都完全相同的内部实现。Node.js 还存在 process.nextTick() 队列,其优先级和 Promise 微任务不同。

四、async/await:Promise 的语法层

async 函数总是返回 Promise:

async function getValue() {
  return 42
}

console.log(getValue() instanceof Promise) // true
getValue().then(value => console.log(value)) // 42

await 会等待一个可被 Promise 解析的值,并暂停当前 async 函数的后续执行;它不会阻塞浏览器主线程,也不会让普通 JavaScript 代码停止运行:

function delay(milliseconds) {
  return new Promise(resolve => {
    setTimeout(resolve, milliseconds)
  })
}

async function run() {
  console.log('run start')
  await delay(10)
  console.log('run after await')
}

run()
console.log('outside')

// run start
// outside
// run after await

await 也可以等待普通值,效果类似于 await Promise.resolve(value)

async function example() {
  const value = await 1
  return value + 1
}

example().then(console.log) // 2

async/await 不会把异步操作变成同步操作,也不代表函数运行在新的线程中;它只是把 Promise 的后续逻辑写成了更容易阅读的形式。

五、三者放在一起时的执行顺序

console.log('script start')

setTimeout(() => {
  console.log('setTimeout')
}, 0)

Promise.resolve().then(() => {
  console.log('promise then')
})

async function task() {
  console.log('async start')
  await null
  console.log('async continuation')
}

task()
console.log('script end')

浏览器和常见 Node.js 环境中通常会看到:

script start
async start
script end
promise then
async continuation
setTimeout

解释:

  1. 顶层同步代码先运行;
  2. 调用 task() 时,await 之前的代码同步运行;
  3. Promise.thenawait 后续都排入微任务,通常按进入队列的先后执行;
  4. 当前任务和微任务完成后,setTimeout 回调才有机会运行。

不能仅凭这段代码推断复杂浏览器渲染时机;渲染是宿主调度的一部分,Node.js 也有自己的事件循环阶段。

六、错误处理方式

6.1 setTimeout 中的异常不会被外层同步 try/catch 捕获

try {
  setTimeout(() => {
    throw new Error('timer error')
  }, 0)
} catch (error) {
  // 不会执行:异常发生在之后的另一个任务中
  console.error(error)
}

需要在回调内部处理,或者把回调包装成 Promise:

function delayAndFail() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error('timer error'))
    }, 0)
  })
}

delayAndFail().catch(error => {
  console.error(error.message)
})

6.2 Promiseasync/await 使用 catchtry/catch

async function loadData() {
  try {
    const response = await fetch('/api/data')
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`)
    }
    return await response.json()
  } catch (error) {
    console.error('load failed:', error)
    throw error
  }
}

loadData().catch(error => {
  console.error('caller handled:', error.message)
})

fetch 遇到 HTTP 404/500 时通常仍会 fulfilled,因此需要自行检查 response.ok;网络失败才会让 fetch Promise rejected。

七、顺序执行和并发执行

7.1 需要前一个结果时顺序等待

async function loadInOrder() {
  const user = await fetch('/api/user').then(response => response.json())
  const orders = await fetch(`/api/orders?user=${user.id}`)
    .then(response => response.json())

  return { user, orders }
}

7.2 互相独立的任务不要无意串行

下面的写法会等待第一个请求完成后才启动第二个请求:

async function loadSlowly() {
  const user = await loadUser()
  const settings = await loadSettings()
  return { user, settings }
}

可以先创建两个 Promise,再使用 Promise.all 并发等待:

async function loadTogether() {
  const userPromise = loadUser()
  const settingsPromise = loadSettings()
  const [user, settings] = await Promise.all([userPromise, settingsPromise])

  return { user, settings }
}

Promise.all 只负责组合结果,不会自动取消失败后仍在进行的其他任务;网络请求需要 AbortController 等取消机制。

7.3 不要把 await 放进 forEach 期待顺序等待

// 常见错误:外层函数不会等待 forEach 中的 async 回调
items.forEach(async item => {
  await save(item)
})

顺序处理使用 for...of

async function saveInOrder(items) {
  for (const item of items) {
    await save(item)
  }
}

需要并发处理并等待全部结果时使用 Promise.all

async function saveTogether(items) {
  await Promise.all(items.map(item => save(item)))
}

八、如何选择

  • 只需要延迟或安排一次宿主任务:使用 setTimeout
  • 需要表达异步结果、链式转换、错误传播或并发组合:使用 Promise;
  • 需要以线性方式阅读 Promise 逻辑:使用 async/await
  • 不要用 setTimeout(fn, 0) 模拟 Promise 微任务或精确计时;
  • 不要把 async/await 当作线程、阻塞式 I/O 或自动取消机制;
  • 生产代码需要同时设计超时、重试、取消、并发上限和错误边界。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS