本文主要内容是对两种请求错误的处理,一是状态码非 200,我们称为「请求错误」;二是状态码为 200,但在业务上请求错误的,如注册时用户名重复、删除商品时商品不存在等我们称为「业务错误」,并且这类错误往往会返回一个 code
来标志错误、一个 msg
表示错误信息。
下面以一个 todolist
为例来具体说明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| import React, { useEffect, useState } from 'react'; import { message } from 'antd';
export default function App() { const [tasks, setTasks] = useState([]);
useEffect(() => { fetchTasks(); }, []); const fetchTasks = async () => { try { const response = await request("http://127.0.0.1:3001/api/tasks"); const { code, msg, data } = response; if (code !== 0) { message.error(msg); return; } const { list } = data; setTasks(list); } catch (err) { message.error(err.message); } };
return ( <div> {tasks.map((task) => { return <div key={task.id}>{task.name}</div>; })} </div> ); }
|
随即我们会意识到这样写在每个地方都要写重复的代码,即判断 code !== 0
表示这次请求是错误的,然后 message.error(msg)
展示错误信息。
这个逻辑在所有存在请求的地方都是一样的,所以我们往往会在请求方法内统一处理这些错误,比如axios
的响应拦截器或者自己实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import axios from "axios"; import { message } from 'antd';
export default function request(url, options = {}) { return axios({ url, ...options }) .then((response) => { const { data: { code, msg, data }, } = response; if (code !== 0) { message.error(msg); return; } return data; }) .catch((error) => { message.error(error.message); }); }
|
这样做,我们在业务层,即我们的页面中就无需关注错误,因为到了页面这里就必然是成功的,不用 try catch
,代码显得简洁优雅。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import React, { useEffect, useState } from "react";
import request from "./utils/request";
export default function App() { const [tasks, setTasks] = useState([]);
useEffect(() => { fetchTasks(); }, []); const fetchTasks = async () => { const data = await request("http://127.0.0.1:3001/api/tasks"); const { list } = data; setTasks(list); };
return ( <div> {tasks.map((task) => { return ( <div key={task.id}>{task.name}</div> ); })} </div> ); }
|
至此似乎很完美了,但业务往往超出我们的预期,现在需要实现对于某个请求,如果请求错误不 message.error
弹出错误 信息,而是在页面上展示,上面的实现肯定是满足不了,那我们就需要修改,很容易我们想到给 request
接收额外 handler
参数,如果传入该参数表示「我要自己处理错误」
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import axios from "axios"; import { message } from 'antd';
export default function request(url, options = {}) { const { handler } = options; return axios({ url, ...options }) .then((response) => { const { data: { code, msg, data }, } = response; if (code !== 0) { if (handler !== undefined) { handler({ code, msg, data }); return; } message.error(msg); return; } return data; }) .catch((error) => { message.error(error.message); }); }
|
业务中使用时额外传入 handler
参数
1 2 3 4 5 6 7 8
| const fetchTaskDetail = (id) => { return request(`http://127.0.0.1:3001/api/tasks/${id}`, { handler: ({ code, msg }) => { }, }); };
|
这样做的确能实现,但有两点不够好
- 在
request.js
中调用了 message.error
等于在底层方法中调用了组件库,不够优雅
- 上手成本,
request
方法居然还有 handler
参数
还有另一种方式,通过 window.addEventListener(‘unhandledRejection’)
,因为错误是有类似「冒泡」机制的,即 a
调用 b
,b
再调用 c
,这样形成所谓的「调用链」,在链条任意节点发生错误,会往上抛出错误,直到被链条其中一环捕获,或者到最顶层。
所以我们在 request.js
中不处理错误,而是将这个错误抛出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import axios from "axios";
export default function request(url, options = {}) { return axios({ url, ...options }).then((response) => { const { data: { code, msg, data }, } = response; if (code !== 0) { return Promise.reject(new Error(msg)); } return data; }); }
|
尝试去触发一个业务错误,可以在控制台看到

这表示存在一个 Promise
错误但我们没有去捕获它,那我们现在来处理它。
在项目入口文件,可以是调用 ReactDOM.render
的地方,如果没有也可以在 App.jsx
这种文件,增加如下代码
1 2 3 4 5 6 7 8 9 10 11
| import { message } from 'antd';
window.addEventListener("unhandledrejection", (event) => { event.preventDefault(); const { reason } = event; if (reason instanceof Error) { message.error(reason.message); return; } message.error(event.message); });
|
没有被处理的错误会被这里处理。
在页面中如果我们希望自己处理错误,只需要将代码块 try ... catch
包裹,表示「我要捕获错误了」
1 2 3 4 5 6 7 8
| const fetchTaskDetail = (id) => { try { return request(`http://127.0.0.1:3001/api/tasks/${id}`); } catch (err) { } };
|
错误不会被 window.addEventListener(‘unhandledrejection’)
重复处理,相比传入 handler
的方式更加「优雅」。
但这仍不是最好的解决方案,当我们 try {}
代码块中出现例如语法错误、类型错误时,同样会被我们的 catch {}
块捕获,并在页面上展示,但这种错误我们其实并不希望在页面上展示,同时如果项目内有错误收集系统,这些错误就被我们吞掉导致无法被收集。
如果是开发环境并使用了 react-error-overlay 的项目,出现业务错误也会展示错误遮罩,非常影响开发体验。
所以对上面的代码再进行优化,
1 2 3 4 5 6 7 8 9 10
| class CodeError { static isCodeError = (error) => { return error instanceOf CodeError; } constructor(params) { Object.assign(this, params); } }
|
首先我们声明一个「业务错误」类,并将之前 code !== 0
时抛出 Error
改成抛出 CodeError
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import axios from "axios";
export default function request(url, options = {}) { return axios({ url, ...options }).then((response) => { const { data: { code, msg, data }, } = response; if (code !== 0) { return Promise.reject(new CodeError({ code, msg })); } return data; }); }
|
之前 message.error
展示错误信息的地方都需要修改
1 2 3 4 5 6 7 8 9 10 11 12 13
| window.addEventListener("unhandledrejection", (event) => { event.preventDefault(); const { reason } = event; if (CodeError.isCodeError(reason)) { message.error(reason.msg); return; } if (reason instanceof Error) { message.error(reason.message); return; } message.error(event.message); });
|
1 2 3 4 5 6 7 8 9 10 11 12
| const fetchTaskDetail = (id) => { try { return request(`http://127.0.0.1:3001/api/tasks/${id}`); } catch (err) { if (CodeError.isCodeError(err)) { return; } throw err; } };
|
在自己处理业务错误时,非业务错误必须通过 throw
再次抛出,这样一些语法错误等就能被错误收集捕获。
这里提供了一段代码,直接在项目中使用即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class CodeError { static isCodeError = (error) => { return error instanceOf CodeError; } constructor(params) { Object.assign(this, params); } }
window.addEventListener("unhandledrejection", (event) => { event.preventDefault(); const { reason } = event; if (CodeError.isCodeError(reason)) { message.error(reason.msg); return; } if (reason instanceof Error) { message.error(reason.message); return; } message.error(event.message); });
|
在项目中使用
1 2 3 4 5
| import { handleError } from 'error-handler';
handleError((error) => { message.error(error.message); });
|
在请求方法中业务错误时抛出该错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import axios from "axios"; import { CodeError } from 'error-handler';
export default function request(url, options = {}) { return axios({ url, ...options }).then((response) => { const { data: { code, msg, data }, } = response; if (code !== 0) { return Promise.reject(new CodeError(msg, { code, msg })); } return data; }); }
|
如果想自己处理错误
1 2 3 4 5 6 7 8
| const fetchTaskDetail = (id) => { return tryCatch(() => { return request(`http://127.0.0.1:3001/api/tasks/${id}`); }).catch((err) => { }); };
|