Axios 的 Promise 编程模型:从 then/catch/finally 到 async/await 的完整实践指南
axios 是一个基于 Promise 的 HTTP 客户端,它构建在 ES6 原生 Promise API 之上:每一次请求都会返回一个 Promise,它要么以响应对象(response object)结算(resolve),要么以错误拒绝(reject)。对于不支持原生 Promise 的环境,需要自行 polyfill(例如 es6-promise)。本文基于仓库中的 Promises 官方文档,结合 Axios.js、settle.js 等源码,系统讲解 axios 的 Promise 编程模型——包括 .then()/.catch()/.finally() 链式处理、async/await 推荐写法、Promise.all 并行请求、Promise.allSettled 容错处理、请求链式编排,以及 AxiosPromise 泛型在 TypeScript 中的类型保真机制,并深入源码说明请求 Promise 是如何被 resolve 或 reject 的。
请求的返回类型:一次请求就是一个 Promise
axios 的每个请求方法(axios.get、axios.post 等)本质上都是对 Axios.prototype.request 的包装,而 request 本身是一个 async 方法,因此调用返回的就是一枚标准的 ES6 Promise:
axios.get("/api/users"); // Promise<AxiosResponse>
从 lib/core/Axios.js 的源码结构看,各 HTTP 方法最终都汇聚到 this.request(...):
// lib/core/Axios.js(节选)
async request(configOrUrl, config) {
try {
return await this._request(configOrUrl, config);
} catch (err) {
// ... 补充/合并错误堆栈后重新 throw
throw err;
}
}
request 内部的 catch 分支还做了一件对排错很有用的事:当捕获到 Error 实例且其 stack 缺失或被截断时,会利用 Error.captureStackTrace 生成当前调用栈并合并进 err.stack。这意味着 .catch((error) => ...) 中拿到的错误对象通常带有更完整的堆栈信息,便于定位抛出点。
而真正决定"这条 Promise 链如何流动"的是 _request 中的拦截器链编排(lib/core/Axios.js 第 192-209 行附近):
let promise;
let i = 0;
let len;
if (!synchronousRequestInterceptors) {
const chain = [dispatchRequest.bind(this), undefined];
chain.unshift(...requestInterceptorChain);
chain.push(...responseInterceptorChain);
len = chain.length;
promise = Promise.resolve(config);
while (i < len) {
promise = promise.then(chain[i++], chain[i++]);
}
return promise;
}
也就是说:当存在异步请求拦截器时,axios 从 Promise.resolve(config) 出发,依次 .then(拦截器 fulfilled, 拦截器 rejected),中间插入 dispatchRequest,最后挂上响应拦截器——整条链上任何一个环节 reject,最终都会传播到你 await 后的 catch 或 .catch()。这正是"axios 返回标准 Promise"这一承诺在源码层面的实现方式。
Promise 的 resolve / reject 由谁决定
请求发出后,Promise 的最终结算发生在适配器层。lib/core/settle.js 是关键的结算函数:
export default function settle(resolve, reject, response) {
const validateStatus = response.config.validateStatus;
if (!response.status || !validateStatus || validateStatus(response.status)) {
resolve(response);
} else {
reject(new AxiosError(
'Request failed with status code ' + response.status,
response.status >= 400 && response.status < 500 ? AxiosError.ERR_BAD_REQUEST : AxiosError.ERR_BAD_RESPONSE,
response.config,
response.request,
response
));
}
}
这里揭示了两个实践要点:
- HTTP 4xx/5xx 不会自动 resolve:只有当
validateStatus(response.status)为真时(默认规则在 lib/defaults/index.js 中定义)才 resolve;否则 reject 一个AxiosError,并根据状态码区分ERR_BAD_REQUEST(4xx)与ERR_BAD_RESPONSE(5xx)。 - 可以在
.catch()中拿到完整上下文:被 reject 的AxiosError携带config、request、response属性,因此文档中.catch((error) => { console.error("Request failed:", error.message); })这类写法可以进一步访问error.response.status、error.config等。
此外,lib/core/dispatchRequest.js 中 adapter(config).then(onAdapterResolution, onAdapterRejection) 还保证了:即使适配器 reject(网络错误、取消等),只要错误对象上挂了 response,也会对其执行 transformResponse 数据转换,然后 Promise.reject(reason) 传播下去——所以"失败响应体也能反序列化"这一行为是有源码背书的。
TypeScript 集成:AxiosPromise 的类型保真
对于 TypeScript 项目,index.d.ts 第 580 行定义了核心类型:
export type AxiosPromise<T = any, D = any, P = any> = Promise<AxiosResponse<T, D, {}, P>>;
AxiosPromise<T, D, P> 就是 AxiosResponse<T, D, {}, P> 的 Promise,其中三个泛型参数分别对应:响应数据类型 T、请求体类型 D、查询参数类型 P。它的关键价值在于:请求的数据和参数会保留在 response.config 上,使类型系统能够追踪一次请求的"完整往返"。
AxiosResponse 接口的定义(index.d.ts 第 515-522 行):
export interface AxiosResponse<T = any, D = any, H = {}, P = any> {
data: T;
status: number;
statusText: string;
headers: (H & RawAxiosResponseHeaders) | AxiosResponseHeaders;
config: InternalAxiosRequestConfig<D, P>;
request?: any;
}
文档给出的示例展示了三个泛型参数如何在实际业务类型中落地:
declare const search: AxiosPromise<SearchResponse, RequestBody, SearchParams>;
search.then((response) => {
response.data; // SearchResponse
response.config.data; // RequestBody | undefined
response.config.params; // SearchParams | undefined
});
从上面的接口定义可以验证:response.data 的类型正是第一个泛型参数 T;response.config 是 InternalAxiosRequestConfig<D, P>,因此 config.data(请求体)与 config.params(查询参数)分别携带 D 与 P 类型。这对"根据本次请求实际发出去什么来推断响应类型"的场景非常有用——同一个接口,config 里保留了本次调用的真实载荷类型。
then / catch / finally:标准的三段式处理
因为 axios 返回标准 Promise,.then()、.catch() 和 .finally() 可以直接使用:
axios.get("/api/users")
.then((response) => {
console.log(response.data);
})
.catch((error) => {
console.error("Request failed:", error.message);
})
.finally(() => {
console.log("Request finished");
});
使用建议:
.then()中处理成功分支,参数是完整的AxiosResponse(data、status、statusText、headers、config);.catch()中处理所有失败分支——包括网络错误、超时、取消(CanceledError)、以及validateStatus判定失败的 4xx/5xx 响应;.finally()适合放置与成败无关的收尾逻辑,如关闭 loading 状态。
async / await:大多数代码库的推荐写法
官方文档推荐在多数代码库中使用 async/await,它让异步代码读起来像同步代码:
async function fetchUser(id) {
try {
const response = await axios.get(`/api/users/${id}`);
return response.data;
} catch (error) {
console.error("Failed to fetch user:", error.message);
throw error;
}
}
这种写法有两个工程价值:
- 控制流更直观:
try/catch覆盖了同步代码和异步错误,避免回调嵌套; - 错误可以精确传播:示例中
catch里先记录日志再throw error,保持了"记录但不吞掉"的错误处理习惯。由于dispatchRequest与settle中 reject 的都是结构化的AxiosError,上游catch可以进一步通过error.isAxiosError、error.code、error.response做精细分支。
并行请求:Promise.all 与 Promise.allSettled
axios 返回标准 Promise,因此可以直接用 Promise.all 同时发起多个请求并等待全部完成:
const [users, posts] = await Promise.all([
axios.get("/api/users"),
axios.get("/api/posts"),
]);
console.log(users.data, posts.data);
注意语义差异:Promise.all 会在任一请求失败时立即 reject。如果需要处理部分失败(部分接口 5xx 不应导致整页崩溃),应改用 Promise.allSettled:
const results = await Promise.allSettled([
axios.get("/api/users"),
axios.get("/api/posts"),
]);
results.forEach((result) => {
if (result.status === "fulfilled") {
console.log(result.value.data);
} else {
console.error("Request failed:", result.reason.message);
}
});
allSettled 为每个请求返回 { status: "fulfilled" | "rejected", value/reason } 结构:成功项的 value 是 AxiosResponse(取 .data),失败项的 reason 是 reject 出的错误对象(通常是 AxiosError,取 .message 或其他属性)。两种方式的取舍可以概括为:
| 方法 | 失败行为 | 适用场景 |
|---|---|---|
Promise.all |
任一失败即整体 reject | 多个请求是"全有或全无"的强依赖组合 |
Promise.allSettled |
等待全部结束,逐项报告结果 | 聚合多个独立数据源,允许局部降级 |
请求链式编排:串联依赖请求
你可以链式调用 .then() 来顺序执行多个请求,把上一个请求的数据传递给下一个——这是"先查用户、再按用户查其文章"这类依赖型数据流的经典写法:
axios.get("/api/user/1")
.then(({ data: user }) => axios.get(`/api/posts?userId=${user.id}`))
.then(({ data: posts }) => {
console.log("Posts for user:", posts);
})
.catch(console.error);
这里有一个容易忽略的细节:在 .then() 的回调中返回一个新的 axios 请求(而不是先赋值再 resolve),Promise 链会自动等待该新请求完成后才进入下一个 .then()。链上任意一环 reject(包括第二步请求失败),都会跳过中间的成功回调直接落到链尾的 .catch()。对应的 async/await 等价写法是:
try {
const { data: user } = await axios.get("/api/user/1");
const { data: posts } = await axios.get(`/api/posts?userId=${user.id}`);
console.log("Posts for user:", posts);
} catch (error) {
console.error(error);
}
两种写法在 Promise 语义上完全等价;选择哪一种是团队风格问题,但错误处理路径(catch 的位置、是否 rethrow)应当保持一致。
小结
- axios 的每次请求都是标准 ES6 Promise:成功 resolve 完整的
AxiosResponse,失败 reject 携带config/response的AxiosError(或取消时的CanceledError); - 4xx/5xx 是否算"失败"由
validateStatus决定,结算逻辑在 lib/core/settle.js 中; - 推荐用
async/await + try/catch组织控制流,用.finally()收尾; - 强依赖的并行请求用
Promise.all,允许局部失败的聚合用Promise.allSettled; - 依赖型顺序请求用
.then()链式编排或等价的await序列; - TypeScript 中用
AxiosPromise<T, D, P>保留响应类型、请求体与查询参数的完整类型链路。
想进一步理解拦截器如何嵌入这条 Promise 链,可以继续查看 interceptors.md 文档与 lib/core/Axios.js 中 _request 的链式编排实现;错误处理细节可参考 error-handling.md。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00