咱們都知道 try catch 沒法捕獲 setTimeout 異步任務中的錯誤,那其中的緣由是什麼。以及異步代碼在 js 中是特別常見的,咱們該怎麼作才比較?前端
function main() { try { setTimeout(() => { throw new Error('async error') }, 1000) } catch(e) { console.log(e, 'err') console.log('continue...') } } main();
這段代碼中,setTimeout 的回調函數拋出一個錯誤,並不會在 catch 中捕獲,會致使程序直接報錯崩掉。vue
因此說在 js 中 try catch 並非說寫上一個就能夠高枕無憂了。難道每一個函數都要寫嗎,
那什麼狀況下 try catch 沒法捕獲 error 呢?node
宏任務的回調函數中的錯誤沒法捕獲react
上面的栗子稍微改一下,主任務中寫一段 try catch,而後調用異步任務 task,task 會在一秒以後拋出一個錯誤。git
// 異步任務 const task = () => { setTimeout(() => { throw new Error('async error') }, 1000) } // 主任務 function main() { try { task(); } catch(e) { console.log(e, 'err') console.log('continue...') } }
這種狀況下 main 是沒法 catch error 的,這跟瀏覽器的執行機制有關。異步任務由 eventloop 加入任務隊列,並取出入棧(js 主進程)執行,而當 task 取出執行的時候, main 的棧已經退出了,也就是上下文環境已經改變,因此 main 沒法捕獲 task 的錯誤。es6
事件回調,請求回調同屬 tasks,因此道理是同樣的。eventloop 複習能夠看這篇文章github
微任務(promise)的回調跨域
// 返回一個 promise 對象 const promiseFetch = () => new Promise((reslove) => { reslove(); }) function main() { try { // 回調函數裏拋出錯誤 promiseFetch().then(() => { throw new Error('err') }) } catch(e) { console.log(e, 'eeee'); console.log('continue'); } }
promise 的任務,也就是 then 裏面的回調函數,拋出錯誤一樣也沒法 catch。由於微任務隊列是在兩個 task 之間清空的,因此 then 入棧的時候,main 函數也已經出棧了。數組
不少人可能有一個誤解,由於大部分遇到沒法 catch 的狀況,都發生在回調函數,就認爲回調函數不能 catch。promise
不全對,看一個最普通的栗子。
// 定義一個 fn,參數是函數。 const fn = (cb: () => void) => { cb(); }; function main() { try { // 傳入 callback,fn 執行會調用,並拋出錯誤。 fn(() => { throw new Error('123'); }) } catch(e) { console.log('error'); } } main();
結果固然是能夠 catch 的。由於 callback 執行的時候,跟 main 還在同一次事件循環中,即一個 eventloop tick。因此上下文沒有變化,錯誤是能夠 catch 的。
根本緣由仍是同步代碼,並無遇到異步任務。
先看兩段代碼:
function main1() { try { new Promise(() => { throw new Error('promise1 error') }) } catch(e) { console.log(e.message); } } function main2() { try { Promise.reject('promise2 error'); } catch(e) { console.log(e.message); } }
以上兩個 try catch 都不能捕獲到 error,由於 promise 內部的錯誤不會冒泡出來,而是被 promise 吃掉了,只有經過 promise.catch 才能夠捕獲,因此用 Promise 必定要寫 catch 啊。
而後咱們再來看一下使用 promise.catch 的兩段代碼:
// reject const p1 = new Promise((reslove, reject) => { if(1) { reject(); } }); p1.catch((e) => console.log('p1 error'));
// throw new Error const p2 = new Promise((reslove, reject) => { if(1) { throw new Error('p2 error') } }); p2.catch((e) => console.log('p2 error'));
promise 內部的不管是 reject 或者 throw new Error,均可以經過 catch 回調捕獲。
這裏要跟咱們最開始微任務的栗子區分,promise 的微任務指的是 then 的回調,而此處是 Promise 構造函數傳入的第一個參數,new Promise 是同步執行的。
那 then 以後的錯誤如何捕獲呢。
function main3() { Promise.resolve(true).then(() => { try { throw new Error('then'); } catch(e) { return e; } }).then(e => console.log(e.message)); }
只能是在回調函數內部 catch 錯誤,並把錯誤信息返回,error 會傳遞到下一個 then 的回調。
const p3 = () => new Promise((reslove, reject) => { setTimeout(() => { reject('async error'); }) }); function main3() { p3().catch(e => console.log(e)); } main3();
把異步操做用 Promise 包裝,經過內部判斷,把錯誤 reject,在外面經過 promise.catch 捕獲。
首先咱們模擬一個請求失敗的函數 fetchFailure,fetch 函數一般都是返回一個 promise。
main 函數改爲 async,catch 去捕獲 fetchFailure reject 拋出的錯誤。能不能獲取到呢。
const fetchFailure = () => new Promise((resolve, reject) => { setTimeout(() => {// 模擬請求 if(1) reject('fetch failure...'); }) }) async function main () { try { const res = await fetchFailure(); console.log(res, 'res'); } catch(e) { console.log(e, 'e.message'); } } main();
async 函數會被編譯成好幾段,根據 await 關鍵字,以及 catch 等,好比 main 函數就是拆成三段。
經過 step 來控制迭代的進度,好比 "next",就是往下走一次,從 1->2,異步是經過 Promise.then() 控制的,你能夠理解爲就是一個 Promise 鏈,感興趣的能夠去研究一下。 關鍵是生成器也有一個 "throw" 的狀態,當 Promise 的狀態 reject 後,會向上冒泡,直到 step('throw') 執行,而後 catch 裏的代碼 console.log(e, 'e.message');
執行。
明顯感受 async/await 的錯誤處理更優雅一些,固然也是內部配合使用了 Promise。
async 函數處理異步流程是利器,可是它也不會自動去 catch 錯誤,須要咱們本身寫 try catch,若是每一個函數都寫一個,也挺麻煩的,比較業務中異步函數會不少。
首先想到的是把 try catch,以及 catch 後的邏輯抽取出來。
const handle = async (fn: any) => { try { return await fn(); } catch(e) { // do sth console.log(e, 'e.messagee'); } } async function main () { const res = await handle(fetchFailure); console.log(res, 'res'); }
寫一個高階函數包裹 fetchFailure,高階函數複用邏輯,好比此處的 try catch,而後執行傳入的參數-函數 便可。
而後,加上回調函數的參數傳遞,以及返回值遵照 first-error,向 node/go 的語法看齊。以下:
const handleTryCatch = (fn: (...args: any[]) => Promise<{}>) => async (...args: any[]) => { try { return [null, await fn(...args)]; } catch(e) { console.log(e, 'e.messagee'); return [e]; } } async function main () { const [err, res] = await handleTryCatch(fetchFailure)(''); if(err) { console.log(err, 'err'); return; } console.log(res, 'res'); }
可是還有幾個問題,一個是 catch 後的邏輯,這塊還不支持自定義,再就是返回值總要判斷一下,是否有 error,也能夠抽象一下。
因此咱們能夠在高階函數的 catch 處作一下文章,好比加入一些錯誤處理的回調函數支持不一樣的邏輯,而後一個項目中錯誤處理能夠簡單分幾類,作不一樣的處理,就能夠儘量的複用代碼了。
// 1. 三階函數。第一次傳入錯誤處理的 handle,第二次是傳入要修飾的 async 函數,最後返回一個新的 function。 const handleTryCatch = (handle: (e: Error) => void = errorHandle) => (fn: (...args: any[]) => Promise<{}>) => async(...args: any[]) => { try { return [null, await fn(...args)]; } catch(e) { return [handle(e)]; } } // 2. 定義各類各樣的錯誤類型 // 咱們能夠把錯誤信息格式化,成爲代碼裏能夠處理的樣式,好比包含錯誤碼和錯誤信息 class DbError extends Error { public errmsg: string; public errno: number; constructor(msg: string, code: number) { super(msg); this.errmsg = msg || 'db_error_msg'; this.errno = code || 20010; } } class ValidatedError extends Error { public errmsg: string; public errno: number; constructor(msg: string, code: number) { super(msg); this.errmsg = msg || 'validated_error_msg'; this.errno = code || 20010; } } // 3. 錯誤處理的邏輯,這可能只是其中一類。一般錯誤處理都是按功能需求來劃分 // 好比請求失敗(200 可是返回值有錯誤信息),好比 node 中寫 db 失敗等。 const errorHandle = (e: Error) => { // do something if(e instanceof ValidatedError || e instanceof DbError) { // do sth return e; } return { code: 101, errmsg: 'unKnown' }; } const usualHandleTryCatch = handleTryCatch(errorHandle); // 以上的代碼都是多個模塊複用的,那實際的業務代碼可能只須要這樣。 async function main () { const [error, res] = await usualHandleTryCatch(fetchFail)(false); if(error) { // 由於 catch 已經作了攔截,甚至能夠加入一些通用邏輯,這裏甚至不用判斷 if error console.log(error, 'error'); return; } console.log(res, 'res'); }
解決了一些錯誤邏輯的複用問題以後,即封裝成不一樣的錯誤處理器便可。可是這些處理器在使用的時候,由於都是高階函數,可使用 es6 的裝飾器寫法。
不過裝飾器只能用於類和類的方法,因此若是是函數的形式,就不能使用了。不過在平常開發中,好比 React 的組件,或者 Mobx 的 store,都是以 class 的形式存在的,因此使用場景挺多的。
好比改爲類裝飾器:
const asyncErrorWrapper = (errorHandler: (e: Error) => void = errorHandle) => (target: Function) => { const props = Object.getOwnPropertyNames(target.prototype); props.forEach((prop) => { var value = target.prototype[prop]; if(Object.prototype.toString.call(value) === '[object AsyncFunction]'){ target.prototype[prop] = async (...args: any[]) => { try{ return await value.apply(this,args); }catch(err){ return errorHandler(err); } } } }); } @asyncErrorWrapper(errorHandle) class Store { async getList (){ return Promise.reject('類裝飾:失敗了'); } } const store = new Store(); async function main() { const o = await store.getList(); } main();
這種 class 裝飾器的寫法是看到黃子毅 這麼寫過,感謝靈感。
若是對 koa 不熟悉,能夠選擇跳過不看。
koa 中固然也能夠用上面 async 的作法,不過一般咱們用 koa 寫 server 的時候,都是處理請求,一次 http 事務會掉起響應的中間件,因此 koa 的錯誤處理很好的利用了中間件的特性。
好比個人作法是,第一個中間件爲捕獲 error,由於洋蔥模型的緣故,第一個中間件最後仍會執行,而當某個中間件拋出錯誤後,我期待能在此捕獲並處理。
// 第一個中間件 const errorCatch = async(ctx, next) => { try { await next(); } catch(e) { // 在此捕獲 error 路由,throw 出的 Error console.log(e, e.message, 'error'); ctx.body = 'error'; } } app.use(errorCatch); // logger app.use(async (ctx, next) => { console.log(ctx.req.body, 'body'); await next(); }) // router 的某個中間件 router.get('/error', async (ctx, next) => { if(1) { throw new Error('錯誤測試') } await next(); })
爲何在第一個中間件寫上 try catch,就能夠捕獲前面中間件 throw 出的錯誤呢。首先咱們前面 async/await 的地方解釋過,async 中await handle()
,handle 函數內部的 throw new Error
或者 Promise.reject()
是能夠被 async 的 catch 捕獲的。因此只須要 next 函數可以拿到錯誤,並拋出就能夠了,那看看 next 函數。
// compose 是傳入中間件的數組,最終造成中間件鏈的,next 控制遊標。 compose(middlewares) { return (context) => { let index = 0; // 爲了每一箇中間件均可以是異步調用,即 `await next()` 這種寫法,每一個 next 都要返回一個 promise 對象 function next(index) { const func = middlewares[index]; try { // 在此處寫 try catch,由於是寫到 Promise 構造體中的,因此拋出的錯誤能被 catch return new Promise((resolve, reject) => { if (index >= middlewares.length) return reject('next is inexistence'); resolve(func(context, () => next(index + 1))); }); } catch(err) { // 捕獲到錯誤,返回錯誤 return Promise.reject(err); } } return next(index); } }
next 函數根據 index,取出當前的中間件執行。中間件函數若是是 async 函數,一樣的轉化爲 generator 執行,內部的異步代碼順序由它本身控制,而咱們知道 async 函數的錯誤是能夠經過 try catch 捕獲的,因此在 next 函數中加上 try catch 捕獲中間件函數的錯誤,再 return 拋出去便可。因此咱們才能夠在第一個中間件捕獲。詳細代碼能夠看下簡版 koa
而後 koa 還提供了 ctx.throw 和全局的 app.on 來捕獲錯誤。
若是你沒有寫錯誤處理的中間件,那可使用 ctx.throw 返回前端,不至於讓代碼錯誤。
可是 throw new Error 也是有優點的,由於某個中間件的代碼邏輯中,一旦出現咱們不想讓後面的中間件執行,直接給前端返回,直接拋出錯誤便可,讓通用的中間件處理,反正都是錯誤信息。
// 定義不一樣的錯誤類型,在此能夠捕獲,並處理。 const errorCatch = async(ctx, next) => { try { await next(); } catch (err) { const { errmsg, errno, status = 500, redirect } = err; if (err instanceof ValidatedError || err instanceof DbError || err instanceof AuthError || err instanceof RequestError) { ctx.status = 200; ctx.body = { errmsg, errno, }; return; } ctx.status = status; if (status === 302 && redirect) { console.log(redirect); ctx.redirect(redirect); } if (status === 500) { ctx.body = { errmsg: err.message, errno: 90001, }; ctx.app.emit('error', err, ctx); } } } app.use(errorCatch); // logger app.use(async (ctx, next) => { console.log(ctx.req.body, 'body'); await next(); }) // 經過 ctx.throw app.use(async (ctx, next) => { //will NOT log the error and will return `Error Message` as the response body with status 400 ctx.throw(400,'Error Message'); }); // router 的某個中間件 router.get('/error', async (ctx, next) => { if(1) { throw new Error('錯誤測試') } await next(); }) // 最後的兜底 app.on('error', (err, ctx) => { /* centralized error handling: * console.log error * write error to log file * save error and request information to database if ctx.request match condition * ... */ });
本文的代碼都存放於此
總的來講,目前 async 結合 promise 去處理 js 的異步錯誤會是比較方便的。另外,成熟的框架(react、koa)對於錯誤處理都有不錯的方式,儘量去看一下官方是如何處理的。
這只是我對 js 中處理異步錯誤的一些理解。不過前端的須要捕獲異常的地方有不少,好比前端的代碼錯誤,cors 跨域錯誤,iframe 的錯誤,甚至 react 和 vue 的錯誤咱們都須要處理,以及異常的監控和上報,以幫助咱們及時的解決問題以及分析穩定性。採起多種方案應用到咱們的項目中,讓咱們不擔憂頁面掛了,或者又報 bug 了,才能安安穩穩的去度假休息😆
最後的最後,blog地址: https://github.com/sunyongjia...