在这里插入图片描述

前端新人必读:彻底搞懂Promise与async/await异步编程(附实战技巧)

引言:为什么你的代码总在“等”?

先讲个真事。
去年冬天,我陪一个实习生小哥调 bug,场景特简单:页面一加载就要调两个接口,拿到用户信息和广告配置,再把用户名塞进广告标题里。结果他写了 60 多行 callback,嵌套七层,最后变量名都写成 data1data2data3……打印出来能当楼梯。
我递给他一杯速溶咖啡,说:“兄弟,你知道自己为什么脱发吗?因为 callback 在薅你头发。”
他苦着脸:“那咋办?”
我:“把 callback 扔进垃圾桶,让 Promise 和 async/await 给你植发。”
两小时后,代码从 60 行缩到 12 行, bug 没了,发量虽然没长回来,但至少不再继续秃。

这个故事每天都在前端圈子里上演。
JavaScript 是单线程的,却偏要干多线程的活儿:接口请求、定时器、文件读写、事件监听……于是“异步”成了刚需。
Promise 和 async/await 就是刚需里的瑞士军刀——不会用,你就永远在“等”:等接口、等时机、等 bug 自己长脚跑掉。
今天这篇,把我过去五年在异步坑里摔过的跤、流过的泪、写过的 TODO 注释,一次性打包给你。
读完你要是还说不会,我把键盘吃了——机械轴的,嘎嘣脆。


JavaScript 异步世界的入场券:认识 Promise 对象

Promise 是啥?
官方话术:Promise 是表示异步操作最终完成或失败的对象。
人话:它是一张“欠条”。你找后端借数据,后端说:“先给你张欠条,到时候要么给你数据(resolve),要么告诉你没戏(reject)。”
你拿着欠条,就能提前安排“成功后干啥”、“失败后干啥”,而不是傻站在门口等到海枯石烂。

// 一张最简陋的欠条
const欠条 = new Promise((resolve, reject) => {
  // 模拟借钱过程
  setTimeout(() => {
    const 钱到账 = Math.random() > 0.3; // 70% 概率成功
    if (钱到账) {
      resolve('5000块');             // 钱到位,把数据传下去
    } else {
      reject('余额不足');            // 钱没到位,把原因传下去
    }
  }, 1000);
});

// 拿着欠条安排后续
欠条
  .then((现金) => console.log('请小姐姐喝奶茶,花了', 现金))
  .catch((理由) => console.log('小姐姐说:', 理由));

上面这段代码,如果你之前只玩过 callback,会突然有种“世界亮了”的感觉:

  • 没有嵌套
  • 错误集中处理
  • 链式调用像写诗

但别急着嗨,Promise 的坑都在细节里,下面咱们一点点拆。


Promise 的三种状态:pending、fulfilled 和 rejected 到底意味着什么

Promise 的一生只有三种状态,像人的感情:

  • pending:暧昧期,啥都没发生
  • fulfilled:表白成功,对象答应你了
  • rejected:表白失败,对方把你拉黑了

关键规则:

  1. 状态一旦改变,就不可逆,像极了爱情
  2. 你在 then 里能拿到成功的“情书”,在 catch 里拿到失败的“好人卡”
  3. 如果你既不写 then 也不写 catch,Promise 会把你当渣男,抛一个 Uncaught (in promise) 报错,浏览器控制台红得发紫
const 心情 = new Promise((resolve, reject) => {
  // 模拟表白
  setTimeout(() => {
    resolve('❤️ 我也喜欢你');
    // 如果写 reject('你是个好人') 就会走向另一个结局
  }, 500);
});

console.log('0ms:', 心情); // pending

心情
  .then((回信) => console.log('500ms:', 回信))
  .catch((发卡) => console.log('500ms:', 发卡));

// 输出顺序:
// 0ms:Promise {<pending>}
// 500ms:❤️ 我也喜欢你

记住:状态变化只跟 resolve/reject 有关,跟你写多少 then 无关。
很多新人以为多写几个 then 就能多收几次结果,其实只能收到一次——Promise 不是复读机。


从回调地狱到优雅链式调用:Promise 的基本使用姿势

先给你看一眼“地狱”长啥样:

// callback 地狱,慎入
getUserId(function (id) {
  getUserInfo(id, function (info) {
    getOrders(info.uid, function (orders) {
      getDetail(orders[0].orderId, function (detail) {
        console.log(detail); // 五层嵌套,眼睛已瞎
      });
    });
  });
});

再给你看一眼“天堂”:

// Promise 链式,像剥洋葱,一层一层又平又直
getUserId()
  .then(id => getUserInfo(id))
  .then(info => getOrders(info.uid))
  .then(orders => getDetail(orders[0].orderId))
  .then(detail => console.log(detail))
  .catch(err => console.error('出错了', err));

链式调用的秘诀:

  1. 每个 then 里 return 一个新的 Promise,就能把数据继续往下传
  2. 任何一个环节报错,都会被最近的 catch 捕获,不用每层都写 if-err
  3. 如果忘了 return,下一层的 then 会收到 undefined,这种 bug 藏得深, Debug 时想跳楼
// 错误示例:忘了 return
getUserId()
  .then(id => {
    getUserInfo(id); // 没 return,后续拿到的是 undefined
  })
  .then(info => console.log(info)); // undefined

手把手教你创建和组合多个 Promise:resolve、reject、all、race 全解析

  1. 快速创建已决 Promise
Promise.resolve(3);           // 直接成功,值为 3
Promise.reject('错了');       // 直接失败,原因为 '错了'
  1. Promise.all——“全部到齐才开饭”
const 任务1 = fetch('/api/user');
const 任务2 = fetch('/api/config');
const 任务3 = fetch('/api/ad');

Promise.all([任务1, 任务2, 任务3])
  .then(([res1, res2, res3]) => Promise.all([res1.json(), res2.json(), res3.json()]))
  .then(([user, config, ad]) => {
    console.log('人到齐,开饭:', user, config, ad);
  })
  .catch(err => console.log('有一个人没到,饭局取消', err));

特点:

  • 所有 Promise 都成功,才算成功,返回值按顺序排列
  • 只要有一个失败,整体就失败,失败原因是第一个失败的那个
  1. Promise.race——“谁先到就吃谁”
const 超时包装 = new Promise((resolve, reject) => {
  setTimeout(() => reject('超时啦'), 5000);
});

Promise.race([fetch('/api/slow'), 超时包装])
  .then(res => res.json())
  .then(data => console.log('成功拿到数据:', data))
  .catch(err => console.log('要么接口跪,要么超时', err));

race 经典用法:给接口加超时控制,谁先跑完就认谁。

  1. Promise.allSettled——“一个都不能少,结果我全都要”
Promise.allSettled([Promise.resolve(1), Promise.reject('错'), Promise.resolve(3)])
  .then(results => {
    results.forEach((r, idx) =>
      r.status === 'fulfilled'
        ? console.log(`任务${idx}成功`, r.value)
        : console.log(`任务${idx}失败`, r.reason)
    );
  });

allSettled 无论成功失败都会等你,适合“批量上报”场景。


async/await 是什么?它和 Promise 有什么关系?

一句话:async/await 是 Promise 的语法糖,让你用同步的写法写异步。
async 函数默认返回一个 Promise,await 会暂停线程(只是看起来像暂停,实际事件循环还在跑)直到 Promise 解决。

// 把之前的链式改写成 async/await,阅读体验直线上升
async function 加载详情() {
  try {
    const id       = await getUserId();
    const info     = await getUserInfo(id);
    const orders   = await getOrders(info.uid);
    const detail   = await getDetail(orders[0].orderId);
    console.log(detail);
  } catch (err) {
    console.error('出错了', err);
  }
}

注意细节:

  • await 只能写在 async 函数里,否则会报语法错误
  • await 后面如果不是 Promise,也会自动用 Promise.resolve 包装
  • try/catch 既能捕获同步异常,也能捕获 await 后的 reject,堪称“一键兜底”

用 async/await 写出同步风格的异步代码:真实项目中的写法示范

  1. 统一封装请求函数
// 基于 fetch 封装,自带超时、错误码处理、token 注入
async function request(url, options = {}, timeout = 8000) {
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), timeout);

  const mergeOpts = {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${localStorage.token}`,
      ...options.headers,
    },
    signal: controller.signal,
  };

  try {
    const res = await fetch(url, mergeOpts);
    clearTimeout(id);
    if (!res.ok) {
      // 把 HTTP 错误统一包装成 Error 对象
      throw new Error(`请求失败 ${res.status}`);
    }
    return await res.json();
  } catch (err) {
    // 网络错误、超时、abort 全走到这里
    console.error('request 错误:', err);
    throw err;   // 继续向上抛,让业务层决定要不要处理
  }
}
  1. 业务层像写同步逻辑一样调用
async function 初始化首页() {
  try {
    // 三个请求无依赖,并发拉取
    const [user, config, ad] = await Promise.all([
      request('/api/user'),
      request('/api/config'),
      request('/api/ad'),
    ]);

    renderUser(user);
    renderConfig(config);
    renderAd(ad);
  } catch (e) {
    showToast('首页数据加载失败,请刷新');
  }
}
  1. 顺序依赖的场景
async function 提交订单() {
  // 1. 先创建订单
  const order = await request('/api/order/create', { method: 'POST', body: JSON.stringify(cart) });
  // 2. 再获取支付参数
  const payParams = await request('/api/pay/sign', { method: 'POST', body: JSON.stringify({ orderId: order.id }) });
  // 3. 调起收银台
  await wechatPay(payParams);
}

Promise 与 async/await 各自的优缺点:什么时候该用谁?

场景 Promise 链 async/await
简单并发 Promise.all 一把梭 需要手动 Promise.all
顺序依赖 链式 then 可读性略差 await 顺序写,直观
错误处理 只能 catch 一次,有时需要多次 try/catch 想包多少就多少
循环+异步 需要递归或 reduce 写复杂 for…of + await 真香
浏览器兼容 IE 全灭,需 polyfill 同样 IE 全灭,需 regenerator
调试 堆栈浅,断点跳来跳去 堆栈深,但 DevTools 已能正确映射

结论:

  • 80% 业务代码用 async/await,阅读成本低,后期维护省头发
  • 需要复杂并发控制、竞态、超时组合,适当穿插 Promise.all/race
  • 库作者封装底层 API 时,返回 Promise 更纯粹,不给用户强加 async 语法

常见陷阱大揭秘:未捕获的 reject、忘记 return、滥用 await 等典型问题

  1. 未捕获 reject
Promise.reject('💥'); // 控制台一片红
// 解决:始终写 catch,或者全局监听
window.addEventListener('unhandledrejection', e => {
  console.error('全局抓到未处理的 reject', e.reason);
});
  1. 忘记 return,导致后续收到空气
async function  wrapper() {
  const res = await fetch('/api');
  // 忘记 return res.json()
}
wrapper().then(data => console.log(data)); // undefined
  1. 滥用 await,把并发串成顺序
// 错误:本来可以一起跑,却被 await 串成糖葫芦
const user   = await request('/user');
const config = await request('/config');   // 白白多等 200ms
  1. 在循环里直接 push Promise,没等结果
const arr = [];
for (const id of ids) {
  arr.push(request(`/item/${id}`)); // ✅ 正确,先塞 Promise
}
const results = await Promise.all(arr);
  1. try/catch 只包一行,导致逻辑中断
try {
  const data = await request('/pay');
} catch (e) {
  console.log('支付失败');
}
// 如果支付失败,后面还会继续执行,可能产生脏数据
// 解决:把后续依赖逻辑全放进 try,或 catch 里 return 提前中断

调试异步代码不再头疼:浏览器 DevTools 中如何追踪 Promise 执行流

  1. 断点打在 await 语句上,DevTools 会停住,本地变量能看到 Promise 状态
  2. Console 里直接输入 Promise.resolve(1) 会立即打印,方便做实验
  3. Network 面板筛选 XHR/fetch,查看真实耗时,配合 race 做超时验证
  4. Performance 面板录制,可以看到微任务(Micro Task)的执行阶梯,理解“事件循环”里 Promise 为什么比 setTimeout 先跑

小技巧:
给关键 Promise 加标记,方便堆栈追踪

const 标记 = (p, name) => {
  p.catch(() => {}); // 避免未捕获
  return p.finally(() => console.log(`[${name}] 完成`));
};

标记(request('/api/user'), '用户接口');

提升开发效率的小技巧:封装通用请求函数、统一错误处理、避免嵌套过深

  1. 业务错误码也抛 Error,保持风格统一
async function  requestWithCode(url, options) {
  const res = await request(url, options);
  if (res.code !== 0) {
    const err = new Error(res.message);
    err.code = res.code;
    throw err;
  }
  return res.data;
}
  1. 自动重试装饰器
function  retry(fn, times = 3) {
  return async function (...args) {
    let lastErr;
    for (let i = 0; i < times; i++) {
      try {
        return await fn(...args);
      } catch (e) {
        lastErr = e;
        console.log(`${i + 1}次重试`);
      }
    }
    throw lastErr;
  };
}

const robustRequest = retry(request, 3);
  1. 并发限流
class  Semaphore {
  constructor(max) {
    this.max  = max;
    this.curr = 0;
    this.queue = [];
  }
  async  acquire() {
    if (this.curr >= this.max) {
      await new Promise(resolve => this.queue.push(resolve));
    }
    this.curr++;
  }
  release() {
    this.curr--;
    const resolve = this.queue.shift();
    if (resolve) resolve();
  }
}

const sem = new Semaphore(5); // 同时只允许 5 个并发
async function  limitedRequest(url) {
  await sem.acquire();
  try {
    return await request(url);
  } finally {
    sem.release();
  }
}

当你遇到“Promise 不执行”或“await 没反应”时,排查思路是什么?

  1. 先看函数有没有被调用
  2. 再看函数体是不是 async,await 只能活在 async 里
  3. 在 await 前面 console.log 一把,确认程序跑到这里没
  4. 检查是不是早被 reject,但你没写 catch,异常吞掉
  5. 确认是不是死循环阻塞了线程,导致微任务队列永远不被清空
  6. 用上文提到的全局 unhandledrejection 监听,把隐匿错误揪出来

让代码更健壮:如何优雅地处理超时、重试和并发限制

把上面 retry、race、Semaphore 组合起来,就能得到一个“能打”的请求模块:

  • 超时用 race
  • 失败用 retry
  • 并发用信号量
async function  superRequest(url) {
  const req = () => request(url);
  const timeout = (t) => new Promise((_, reject) => setTimeout(() => reject('超时'), t));
  const robust = retry(req, 3);
  return Promise.race([robust(), timeout(8000)]);
}

// 业务层再控制并发
const urls = ['a', 'b', 'c', 'd', 'e'];
await Promise.all(urls.map(u => limitedRequest(u)));

异步编程不只是语法:理解事件循环对 Promise 执行顺序的影响

来看一道经典面试题:

console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);

输出顺序:1 → 4 → 3 → 2
原因:

  • 同步代码最先跑,所以 1、4 马上打印
  • Promise.then 属于微任务(MicroTask),在当前宏任务结束后立即全部清空
  • setTimeout 属于宏任务,哪怕延时 0,也要等下一轮事件循环

理解这个顺序,你就明白为什么“await 后面的代码”总是在“当前函数体”之后、却又在“setTimeout”之前执行。
掌握事件循环,才能真正把异步代码写得“可控”,而不是靠蒙。


别再被面试官问倒:几个高频 Promise 面试题背后的原理剖析

  1. Promise.resolve(1).then(() => 2).then(console.log) 打印啥?
    答:2。因为 then 的返回值会包装成 Promise 继续往下传。

  2. 如何让 Promise.all 在单个失败时依然跑完全部?
    答:用 allSettled,或者提前在每个 Promise 里 catch 掉错误,返回标记值。

  3. 手写一个 Promise.all(简化版)

function  myAll(promises) {
  return new Promise((resolve, reject) => {
    const ret = [];
    let count = 0;
    promises.forEach((p, idx) => {
      Promise.resolve(p)
        .then(data => {
          ret[idx] = data;
          if (++count === promises.length) resolve(ret);
        })
        .catch(reject);
    });
  });
}
  1. async 函数里 return 1,外部得到的是?
    答:Promise(1)。async 永远返回 Promise。

  2. 如何实现 Promise 的取消?
    原生 Promise 不支持取消,需要借助第三方库(如 bluebird)或 AbortController。业务层最好封装“可取消”的高阶函数,避免内存泄漏。


写异步代码也能有仪式感:团队协作中推荐的 Promise 编码规范

  1. 统一返回 Promise:所有异步函数都返回 Promise,别让同事猜
  2. 错误信息标准化:抛 Error 对象,带 code、message、detail,方便日志上报
  3. catch 职责单一:底层只抛不处理,业务层集中 toast/上报
  4. 并发必须加评论:为什么用 all、为什么用 race,让后人看懂
  5. 拒绝“裸 await”:公共库内部如果不需要顺序,用 Promise.all 提速
  6. 注释里写清依赖顺序:尤其是链式调用,标好“1. 获取 token 2. 拉订单 3. 刷新本地缓存”
  7. Code Review 红线:看见嵌套 then 直接打回;看见没有 catch 直接打回;看见 forEach 里 await 直接打回

尾声:把异步代码写得像诗,把头发留在头上

Promise 和 async/await 不是“新语法”,而是“新生活方式”。
它们让你把“等”的焦虑,变成“掌控”的从容;
让你把嵌套的迷宫,拉成一条直路;
让你把凌晨两点的 console,变成十点准时的关机。

当然,工具再好,也架不住滥用。
记住三句话:

  • 并发别串行,头发会喊疼
  • 错误要兜底,不然背锅到白头
  • 注释写清楚,后人少秃头

愿你在未来的代码里,
不再被回调薅头发,
不再被未捕获 reject 吓破胆,
不再被“为什么接口又超时”折磨到失眠。

愿你写完最后一行 await
安心合上电脑,
去拥抱夜色,
以及——
尚未秃头的自己。

欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。

推荐:DTcode7的博客首页。
一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!


专栏系列(点击解锁) 学习路线(点击解锁) 知识定位
《微信小程序相关博客》 持续更新中~ 结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等
《AIGC相关博客》 持续更新中~ AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结
《HTML网站开发相关》 《前端基础入门三大核心之html相关博客》 前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识
《前端基础入门三大核心之JS相关博客》 前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。
通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心
《前端基础入门三大核心之CSS相关博客》 介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页
《canvas绘图相关博客》 Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化
《Vue实战相关博客》 持续更新中~ 详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅
《python相关博客》 持续更新中~ Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具
《sql数据库相关博客》 持续更新中~ SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能
《算法系列相关博客》 持续更新中~ 算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维
《IT信息技术相关博客》 持续更新中~ 作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识
《信息化人员基础技能知识相关博客》 无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方
《信息化技能面试宝典相关博客》 涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面
《前端开发习惯与小技巧相关博客》 持续更新中~ 罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等
《photoshop相关博客》 持续更新中~ 基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结
日常开发&办公&生产【实用工具】分享相关博客》 持续更新中~ 分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具

吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!

在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐