iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 7
0
Software Development

今晚我想來點 Express 佐 MVC 分層架構系列 第 7

[今晚我想來點 Express 佐 MVC 分層架構] DAY 07 - Express 錯誤處理

說到 Express 的錯誤處理機制,就要先來談談 node.js 的 錯誤優先處理(Error-First),這是一種風格,大致上是長這樣的:

const main = (num, cb) => {
  if ( !num ) {
    cb('有錯誤!');
  } else {
    cb(null, {});
  }
};

// 這裡的 Callback 即 Error-First Callback
main(0, (err, data) => {
  if ( err ) {
    throw new Error(err);
  } else {
    console.log('過關!');
  }
});

沒錯,就是把錯誤的參數放在 函式第一個 ,在 node.js 裡面會有許多這樣設計的套件,看似沒什麼大礙卻有很可怕的地雷,當有很多層的 Error-First Callback 的時候,就會產生所謂的 回呼地獄(Callback Hell) ,很難閱讀,維護起來更是要人命/images/emoticon/emoticon16.gif
所以在這邊呼籲大家...
如果可以用 Promise 或 async/await 的話,絕對優先使用!
如果可以用 Promise 或 async/await 的話,絕對優先使用!
如果可以用 Promise 或 async/await 的話,絕對優先使用!

Express 拋出錯誤的方式

在 Express 中拋出錯誤的方式大致上可以分成兩種,會在下方進行示範,並在 app.routing.ts 中進行測試:

直接拋出

透過 throw 進行拋出:

router.get('/error', (req, res, next) => {
  throw 'error page.';
});

next 拋出

next() 帶入錯誤訊息,Express 會視為錯誤:

router.get('/error', (req, res, next) => {
  next('error page.');
});

結果

在瀏覽器中輸入 http://localhost:3000/error 呈現結果:
https://ithelp.ithome.com.tw/upload/images/20200811/20119338Ouxj8tbO19.png

拋出錯誤的撇步

這個方法限定用 Promise,不能用於 Error-First Callback

如果今天有一連串的資料要處理,並且用 then chain 的方式處理,大致上會長這樣:

router.get('/data/error', (req, res, next) => {
  // Fake API
  const getProfile = new Promise((resolve, reject) => {
    setTimeout(() => resolve({ name: 'HAO', age: 22 }), 100);
  });
  const getFriends = new Promise((resolve, reject) => {
    setTimeout(() => resolve([]), 120);
  });
  const errorRequest = new Promise((resolve, reject) => {
    setTimeout(() => reject('Oops!'), 2000);
  });

  getProfile
  .then(profile => getFriends)
  .then(friends => errorRequest)
  .then(() => res.send('GoGoGo!'))
  .catch(err => next(err));
});

如果比較講究 (龜毛) 的人就會希望不要看到一堆 next(err),那可以怎麼做呢?運用 await/async 的機制可以辦到,先定義一個函式:

const errorHandler = (func: (req: Request, res: Response, next: NextFunction) => Promise<void>) => (req: Request, res: Response, next: NextFunction) => func(req, res, next).catch(next);

並把 middleware 帶入此函式中:

router.get('/data/error/promise', errorHandler(async (req, res, next) => {
  // Fake API
  const getProfile = new Promise((resolve, reject) => {
    setTimeout(() => resolve({ name: 'HAO', age: 22 }), 100);
  });
  const getFriends = new Promise((resolve, reject) => {
    setTimeout(() => resolve([]), 120);
  });
  const errorRequest = new Promise((resolve, reject) => {
    setTimeout(() => reject('Oops!'), 2000);
  });

  const profile = await getProfile;
  const friends = await getFriends;
  const none = await errorRequest;
  res.send('GoGoGo!');
}));

兩者結果相同,但前者會將錯誤處理直接放在 middleware 裡,而後者是將 middleware 包起來,並執行一個被包裝好的 async function,只要發生錯誤就會自動將錯誤傳至 next() 中。下方為執行結果:
https://ithelp.ithome.com.tw/upload/images/20200812/20119338ISy6rK6SXk.png

Express 全域錯誤處理

Express 本身有內建錯誤處理器,只要拋出錯誤就會將錯誤資訊帶入其中,這個就是 全域錯誤處理 機制,以一個龐大的系統而言,建議自行設計一套全域錯誤處理機制來格式化錯誤資訊,再送回客戶端。究竟要如何設計呢?只需要在 應用程式層 用中介軟體來解決,這個中介軟體正是前面所提到的 Error-First Callback

app.use(function(err: any, req: Request, res: Response, next: NextFunction) {
  res.status(500).json({ message: err.message || err });
});

現在,打開瀏覽器並進入剛剛設計的 http://localhost:3000/data/error 來測試看看是不是變成 JSON 格式了:
https://ithelp.ithome.com.tw/upload/images/20200812/201193384VK5B9VUOS.png

小結

錯誤處理是很重要的一部份,只要用一套完善的處理機制,不論對開發人員或是使用者都會非常友善,所以務必要做好錯誤處理呦!下一篇將會介紹不同網域之間取資源的問題,在 Express 中該如何解決?敬請期待!


上一篇
[今晚我想來點 Express 佐 MVC 分層架構] DAY 06 - Express 與 body-parser
下一篇
[今晚我想來點 Express 佐 MVC 分層架構] DAY 08 - Express CORS
系列文
今晚我想來點 Express 佐 MVC 分層架構30

尚未有邦友留言

立即登入留言