上周五下午,我正准备下班,产品经理突然跑过来:"用户反馈说提交订单后没反应,是不是又出 bug 了?"
我一查日志,发现接口报了 500 错误,但页面上什么提示都没有。
原来是我写异步请求时忘了加try-catch。用户点完提交就以为成功了,结果订单根本没生成。
那一刻的我才意识到:async/await 错误处理真的不能省。
先来理解 async/await 是什么
简单来说,async/await是处理异步操作的语法糖,让异步代码看起来像同步代码一样直观。
没有 async/await 的时代:
javascript
// 回调地狱
fetchData(function(result1) {
fetchMoreData(result1, function(result2) {
fetchEvenMoreData(result2, function(result3) {
// 更多嵌套...
})
})
})
有了 async/await 之后:
javascript
// 同步般的写法
async function getData() {
const result1 = await fetchData()
const result2 = await fetchMoreData(result1)
const result3 = await fetchEvenMoreData(result2)
return result3
}
是不是清爽多了?但是,如果 await 后面的 Promise 发生错误(比如网络请求失败),这个错误会直接抛出,如果不捕获,就会导致程序崩溃。
什么时候必须加 try-catch?
1. 需要给用户明确反馈的场景
举个例子:用户点击提交按钮,如果失败了却没有任何提示,用户会以为提交成功,这体验多差啊!
html
<template>
<div>
<button @click="submitOrder" :disabled="loading">
{{ loading ? '提交中...' : '提交订单' }}
</button>
</div>
</template>
<script>
export default {
data() {
return {
loading: false,
successMessage: '',
errorMessage: ''
}
},
methods: {
async submitOrder() {
// 开始加载
this.loading = true
this.errorMessage = ''
try {
// 尝试提交订单
const result = await this.$http.post('/api/orders', this.orderData)
// 提交成功
this.successMessage = '订单提交成功!'
this.$router.push('/success') // 跳转到成功页面
} catch (error) {
// 根据不同错误类型给用户不同的提示
if (error.response?.status === 401) {
this.errorMessage = '请先登录后再提交订单'
} else if (error.response?.status === 400) {
this.errorMessage = '订单数据有误,请检查后重试'
} else if (error.response?.status === 500) {
this.errorMessage = '服务器繁忙,请稍后重试'
} else {
this.errorMessage = '网络错误,请检查网络连接'
}
} finally {
// 无论成功失败,都要取消加载状态
this.loading = false
}
}
}
}
</script>
关键点:
- 用户操作必须有反馈
- 不同错误给出不同提示
- 使用 finally 确保加载状态正确重置
2. 需要继续执行后续逻辑的场景
有时候,即使某个请求失败了,我们仍然希望继续执行其他操作。
javascript
async function initializePage() {
// 获取用户基本信息(重要)
try {
this.userInfo = await this.$http.get('/api/user/info')
} catch (error) {
console.error('获取用户信息失败,但页面仍可正常使用')
// 即使失败,也继续执行下面的逻辑
}
// 获取用户设置(重要)
try {
this.userSettings = await this.$http.get('/api/user/settings')
} catch (error) {
console.error('获取用户设置失败')
// 使用默认设置继续
this.userSettings = this.defaultSettings
}
// 获取推荐内容(非关键,失败也没关系)
try {
this.recommendations = await this.$http.get('/api/recommendations')
} catch (error) {
// 静默失败,不影响主要功能
console.warn('推荐内容加载失败')
}
}
什么时候可以不加 try-catch?
1. 有全局错误拦截器的情况
如果你的项目配置了全局的 HTTP 拦截器,那么很多错误已经被统一处理了。
javascript
// http.js - 全局拦截器
this.$http.interceptors.response.use(
response => response,
error => {
// 全局统一处理错误
if (error.response?.status === 401) {
router.push('/login')
} else if (error.response?.status >= 500) {
Message.error('服务器错误,请稍后重试')
}
return Promise.reject(error)
}
)
// 组件中 - 不需要重复处理
async fetchData() {
// 错误已经被全局拦截器处理了
const data = await this.$http.get('/api/data')
this.list = data
}
2. 错误需要向上抛出的情况
在编写可复用的函数时,通常不应该在函数内部处理错误,而是让调用方来决定如何处理。
javascript
// api/user.js - 用户相关的 API 函数
export const userApi = {
// 不处理错误,让调用方决定如何处理
async getUserProfile(userId) {
const response = await this.$http.get(`/api/users/${userId}`)
return response.data
},
async updateUserProfile(userId, profile) {
const response = await this.$http.put(`/api/users/${userId}`, profile)
return response.data
}
}
// 组件中 - 调用方处理错误
export default {
methods: {
async loadUserProfile() {
try {
this.profile = await userApi.getUserProfile(this.userId)
} catch (error) {
this.$message.error('加载用户信息失败')
}
}
}
}
让代码更简洁
每次都写 try-catch 确实有点啰嗦。我们可以封装一个工具函数:
js
// utils/safeAsync.js
export function safeAsync(promise) {
return promise
.then(data => [null, data]) // 成功:[null, 数据]
.catch(err => [err, null]) // 失败:[错误, null]
}
使用方式:
js
import { safeAsync } from '@/utils/safeAsync'
async function loadUser() {
const [err, data] = await safeAsync(getUserInfo())
if (err) {
ElMessage.error('加载失败')
return
}
user.value = data
}
这看起来更舒服了吧?这种写法来自 Go 语言的错误优先风格,在 JS 社区也很流行。
多个请求怎么办?先别用 Promise.all
很多朋友喜欢这样写:
js
// 危险!一个失败,全部失败
const [user, orders] = await Promise.all([
getUser(),
getOrders()
])
但如果 getOrders() 挂了,getUser() 的结果也会丢掉!
正确做法:用 Promise.allSettled
js
const results = await Promise.allSettled([getUser(), getOrders()])
const user = results[0].status === 'fulfilled' ? results[0].value : null
const orders = results[1].status === 'fulfilled' ? results[1].value : []
// 即使订单加载失败,用户信息还能显示!
或者用我们上面的 safeAsync:
js
const [userErr, user] = await safeAsync(getUser())
const [orderErr, orders] = await safeAsync(getOrders())
if (userErr) ElMessage.warning('用户信息加载失败')
if (orderErr) ElMessage.warning('订单加载失败')
全局错误兜底
即使你写了 try-catch,也可能漏掉。所以可以做一些兜底的操作。
比如在全局拦截器处理,或者也可以在 main.js 这样写加个安全网:
js
// main.js
const app = createApp(App)
// 全局 Vue 错误处理器
app.config.errorHandler = (err, instance, info) => {
console.error('Vue 组件错误:', err, info)
ElNotification.error({
title: '系统异常',
message: '页面出现错误,请刷新重试'
})
}
app.mount('#app')
这样,就算你忘了加 try-catch,也不会让用户看到白屏!
总结
- 用户需要反馈时必须加 try-catch
- 关键业务流程必须加 try-catch
- 有全局处理时可以不加,避免重复
- 编写可复用函数时通常不加,让调用方处理
- 非关键操作可以不加,或简单处理
错误处理不是一刀切的事情,需要根据具体业务场景来决定。好的错误处理能让你的应用更加健壮,用户体验更好。
感谢观看,希望这篇文章能帮你理清思路!
本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!
📌往期精彩
《代码里全是 new 对象,真的很 Low 吗?我认真想了一晚》
《Java 开发必看:什么时候用 for,什么时候用 Stream?》