try...catch 抓不到 Promise 的錯(cuò)誤?原來(lái)是這么回事
在 JavaScript 中,try...catch 是我們處理錯(cuò)誤的得力助手。我們很自然地認(rèn)為,只要把可能出錯(cuò)的代碼放進(jìn) try 塊,catch 就一定能捕獲到異常。但當(dāng)你開(kāi)始和 Promise 打交道時(shí),可能會(huì)遇到一個(gè)讓你困惑的場(chǎng)景:
try {
// 假設(shè)這是一個(gè)會(huì)失敗的 API 請(qǐng)求
fetch('https://non-existent-url.com/api');
console.log('請(qǐng)求已發(fā)送');
} catch (error) {
// 這里的 catch 會(huì)執(zhí)行嗎?
console.log('抓到錯(cuò)誤了!', error);
}
// 控制臺(tái)輸出:
// 請(qǐng)求已發(fā)送
// Uncaught (in promise) TypeError: Failed to fetch
咦?catch 塊根本沒(méi)有執(zhí)行!錯(cuò)誤信息直接在控制臺(tái)炸開(kāi)了,帶著一個(gè)扎眼的 Uncaught (in promise)。
這究竟是為什么?難道 try...catch 對(duì) Promise 無(wú)效嗎?
別急,這并非 try...catch 的 bug,而是我們對(duì) 同步 與 異步 的理解出了偏差。
核心原因:try...catch 是同步的,而 Promise 是異步的
讓我們用一個(gè)更簡(jiǎn)單的比喻來(lái)理解:
你點(diǎn)了一份外賣(mài)(發(fā)起一個(gè) Promise 請(qǐng)求)。try...catch 就像你家門(mén)口的保安。
- 你下單的動(dòng)作是瞬間完成的。你按下“支付”按鈕,App 立刻告訴你“下單成功,騎手正在路上”。這個(gè)“下單成功”的反饋是 同步 的。
- 保安 try...catch 只在你下單的那個(gè)瞬間盯著你。他看到你成功下了單,沒(méi)出任何問(wèn)題(比如網(wǎng)絡(luò)斷了、余額不足等),于是他就下班了。
- 半小時(shí)后,騎手送餐路上翻車(chē)了(Promise 狀態(tài)變?yōu)?rejected)。這個(gè)錯(cuò)誤發(fā)生在未來(lái),發(fā)生在保安下班之后。保安自然是抓不到這個(gè)“錯(cuò)誤”的。
回到代碼中:
- try { ... } 塊里的代碼是 同步執(zhí)行 的。
- fetch(...) 這個(gè)函數(shù)被調(diào)用時(shí),它 立即返回 一個(gè) Promise 對(duì)象。在 try 塊看來(lái),這個(gè)返回動(dòng)作是成功的,沒(méi)有任何錯(cuò)誤被“拋出”(throw)。
- 所以,try 塊順利執(zhí)行完畢,catch 自然不會(huì)被觸發(fā)。
- 真正的網(wǎng)絡(luò)錯(cuò)誤發(fā)生在稍后的某個(gè)時(shí)間點(diǎn),當(dāng)這個(gè)錯(cuò)誤發(fā)生時(shí),它改變了那個(gè)已經(jīng)返回的 Promise 對(duì)象的狀態(tài),將其置為 rejected。這個(gè)錯(cuò)誤屬于 異步世界,而同步的 try...catch早已執(zhí)行完畢,鞭長(zhǎng)莫及。
正確的姿勢(shì):使用 async/await
那么,如何讓保安(try...catch)等到外賣(mài)送到(或出事)再下班呢?答案就是使用 async/await。
await 關(guān)鍵字有一個(gè)神奇的魔力:它會(huì)“暫停”當(dāng)前 async 函數(shù)的執(zhí)行,直到它等待的 Promise 有了結(jié)果(無(wú)論是成功 resolved 還是失敗 rejected)。
如果 Promise 失敗了,await 會(huì)像一個(gè)“信使”,把這個(gè)異步的錯(cuò)誤“解包”并 重新在當(dāng)前同步上下文中拋出。這樣一來(lái),try...catch 就能穩(wěn)穩(wěn)地接住它了。
讓我們來(lái)改造一下代碼:
看,這次 catch 完美地捕獲了錯(cuò)誤!
async/await 的工作流程:
- 函數(shù)用 async 標(biāo)記,表示這是一個(gè)異步函數(shù)。
- await 守在 fetch(...) 前面,函數(shù)執(zhí)行到這里就“暫?!绷耍粫?huì)阻塞整個(gè)程序。
- 它耐心等待 fetch 返回的 Promise 結(jié)果。
- 當(dāng) Promise 因?yàn)榫W(wǎng)絡(luò)問(wèn)題而 rejected 時(shí),await 將這個(gè) rejection 的原因(也就是那個(gè) error 對(duì)象)作為一個(gè)同步錯(cuò)誤 throw 出來(lái)。
- 這個(gè)被 throw 出來(lái)的錯(cuò)誤,正好在 try 塊的作用域內(nèi),于是被 catch 成功捕獲。
別忘了還有 .catch() 方法
當(dāng)然,處理 Promise 錯(cuò)誤并非只有 async/await 這一條路。在 async/await 出現(xiàn)之前,我們一直使用 Promise 自帶的 .catch() 方法鏈?zhǔn)秸{(diào)用來(lái)處理錯(cuò)誤,這同樣非常有效。
fetch('https://non-existent-url.com/api')
.then(response => {
if (!response.ok) {
// 手動(dòng)拋出一個(gè)錯(cuò)誤,讓下面的 .catch() 捕獲
throw new Error('網(wǎng)絡(luò)響應(yīng)不佳');
}
return response.json();
})
.then(data => {
console.log('請(qǐng)求成功:', data);
})
.catch(error => {
// 任何在 .then() 鏈中發(fā)生的錯(cuò)誤都會(huì)在這里被捕獲
console.log('在 .catch() 方法中抓到錯(cuò)誤了!', error);
});
這種方式的優(yōu)點(diǎn)是代碼結(jié)構(gòu)清晰,形成了一條“成功路徑” (.then) 和一條“失敗路徑” (.catch)。