如何優雅處理JavaScript異步錯誤?

1. try/catch

try/catch基本上是你們最常和async/await一塊兒使用的,基本上咱們會用它去包圍大部分的異步方法。await關鍵字後面的promise一旦reject了,就會拋出一個異常錯誤。javascript

run();
async function run() {
    try {
        await Promise.ject(new Error('Oops!'));
    } catch (err) {
        console.error(error.message);
    }
}
複製代碼

try/catch一樣也能夠處理同步的錯誤,好比下面 :html

async function run() {
  const v = null;
  try {
    await Promise.resolve('foo');
    v.thisWillThrow;
  } catch (error) {
        // 會出現"TypeError: Cannot read property 'thisWillThrow' of null"
      console.error(error.message);
  }
}
複製代碼

return問題java

好像咱們只要無腦把邏輯都放到try/catch裏面就萬事大吉了嗎?不太準確,下面的代碼卻會致使unhandled promise rejection。這個return關鍵字直接返回就錯誤卻不會被捕獲:node

async function run() {
    try {
        // 直接返回Promise,而不是用await關鍵字
        return Promise.reject(new Error('Oops!'));
    } catch (error) {
        console.error(error.message);        
    }
}
複製代碼

這裏的解決方式是使用return await來解決:golang

async function run() {
    try {
        return await Promise.reject(new Error('Oops!'));
    } catch (error) {
        console.error(error.message);        
    }
}
複製代碼

回調問題api

另一個問題是try catch捕獲不了回調函數。try catch 僅僅在單一執行環境中奏效。這裏是在回調中加入try catch 來捕獲錯誤:跨域

setTimeout(funciton() {
  try {
    fn()
  } catch (e) {
      // handle error
  }          
           
})
複製代碼

這是奏效的,不過try catch會在各個地方都出現。而V8引擎是不鼓勵try catch在函數中的使用的。 以前試過把try catch移到頂層來捕獲調用棧的錯誤,但這個處理對異步代碼不會奏效。數組

2. Golang-style(then)

golang style即便用.then()的方法來將一個promise轉換爲另外一個處理完錯誤的reject promise。可使用相似if(err)來進行檢查:promise

async function throwAnError() {
    throw new Error('Opps!');
}

async function runAwait() {
    let err = await throwAnError();
    if (err){
       console.error(err.message);
    }
}
複製代碼

這麼寫會直接拋出異常,由於這個方法拋出了異常,可是該方法自己沒有用try/catch捕獲。不少時候,咱們在使用第三方庫的時候可能會出現這種狀況。瀏覽器

then()解決方法

async function runAwait() {
       let err = await throwAnError().then(() => null, err => err);
    if (err){
       console.error(err.message);
    }
}


複製代碼

then()的方式,就會等待promise狀態resolvereject後而後執行相應的回調,而後判斷err對象並處理,因此其實它至關於被捕獲了。

同時返回錯誤和值

async function run() {
    let [err, res] = await throwAnError().then(v => [null, v], err => [err, null]);
    if (err){
        console.error(err.message);
    }
    console.log(res)
}
複製代碼

結果:

這麼作能夠經過解構返回一個數組,包含告終果和error對象。固然若是是reject就會返回nullerror對象;而若是resolved返回數組的第一個error對象就爲null,第二個就是結果。

優缺點

  • 優勢:這種模式能夠更簡潔地處理,同時能夠不須要寫catch
  • 缺點1:這是很是重複性的,每次執行異步操做都須要去判斷error對象。
  • 缺點2:沒法幫助處理run方法中的同步錯誤。 因此這種方式須要謹慎使用。

3. Catch捕獲

上面兩種模式均可以處理異步錯誤,可是對於錯誤處理,最好的狀況是在異步邏輯的最後加上catch,這樣能夠保證全部錯誤都被捕獲到。其實這也是一個原則,即統一處理錯誤,而不是單獨去判斷並處理每一個錯誤

async function run() {
  return Promise.reject(new Error('Oops!'));
}

run().catch(function handleError(err) {
    console.error(err.message);
}).catch( err => {
    process.nextTick(() => { throw errl});
})

複製代碼

使用catch捕獲錯誤,若是handleError自己也有錯誤,就須要再catch一遍,可是爲了不回調地獄,若是該方法發生了錯誤就終止該進程。

優缺點

  • 使用catch的話,無論異步方法自己是否捕獲錯誤,它都會去捕獲異步錯誤。
  • 使用try/catch沒法避免catch自己拋出異常,而若是它拋出了那除了嵌套多一層try/catch外,最好的作法就是加catch來讓代碼更簡潔。

4  全局錯誤捕獲

4.1 瀏覽器全局錯誤捕獲

瀏覽器全局處理基本上就是依靠事件,由於瀏覽器是事件驅動的。一旦拋出錯誤,解釋器在執行環境上下文中中止執行並展開,此時會有一個onerror全局事件拋出:

window.addEventListener('error', function (e) {
    var error = e.error;
    console.log(error);
})
複製代碼

全局錯誤處理器會捕獲任何在執行環境中發生的錯誤,即使是不一樣的對象發生的錯誤事件,或者是各類類型的錯誤。這是全局集中處理錯誤的一種常見方式。

調用棧

調用棧在定位問題的時候十分重要,咱們可使用調用棧在處理器中處理特定的錯誤。

window.addEventListener('error', function (e) {
  var stack = e.error.stack;
  var message = e.error.toString();
  if (stack) {
    message += '\n' + stack;
  }
  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/log', true);
  // Fire an Ajax request with error details
  xhr.send(message);
});
複製代碼

經過日誌能夠看到,具體什麼狀況觸發了什麼錯誤。在調試時調用堆棧也會很是有用。你 能夠分析log,看到什麼條件下觸發了錯誤。

注意:

若是跨域腳本是不會看到錯誤的。 在JS中,錯誤信息僅僅是容許在同一個域中。

我的想法

更多的時候,代碼拋出了異常,咱們更關注的是在運行時,某個變量的值是什麼,是否這個變量的值致使了錯誤,因此打印出調用時的跟多的信息更重要。

4.2 Node.js全局錯誤捕獲

Node.js自己的異常處理要複雜得多,由於涉及到了進程或線程拋出異常的問題。

基於Koa的全局錯誤處理

nodejs是error-first的異步處理機制,此處底層會調用net模塊的listen方法並在錯誤發生時執行回調。

app.listen(app.config.listenPort, (err) => {
  if (err) throw err
  app.logger.info(`> Ready on http://localhost:${app.config.listenPort}`)
})
複製代碼

路由錯誤處理

對於每一個路由,它可能也會有不一樣的錯誤處理邏輯,這時路由進來的請求就須要根據狀況返回不一樣的異常碼和信息。

router.get('/loginAuth', async (ctx, next) => {
  try {
    const code = query.code
    const res = await requestToken(code)
    if (res.data.code !== 0) {
      ctx.app.logger.error(`request token error.Code is ${res.data.code} || response is: ${JSON.stringify(res.data.data)} || msg: ${res.data.message}`)
      ctx.body = {
        code: 10000,
        message: `request token by code error`
      }
    } else {
      ctx.body = res.data
    }
  } catch (err) {
    ctx.app.logger.error(`request api has exception ${ctx.request.url} || ${err.code} || ${err.message} || ${err.stack}`)
    ctx.body = {
      code: 500,
      message: `Error response`
    }
  }
})
複製代碼

5. 總結

  • 一般異常多是預期的或者超出預期的,無論怎樣,使用try/catch沒有問題。
  • 對於超出預期的錯誤,儘可能使用catch來保證它們會被捕獲到。
  • 把錯誤處理器添加到window對象上,它會捕獲到異步錯誤,符合了DRYSOLID原則。一個全局的錯誤處理器能夠幫你保持異步代碼整潔。

Reference

async-await-error-handling

nodejs-v12-lts

相關文章
相關標籤/搜索