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