前後端可能發生的錯誤有千百種,為了方便維護,還有統一管理,我決定在 Isomorphic 的 Flux Store 中掛上 errors 陣列,無論是前端還是後端,發生了任何錯誤,只要 Dispatch Action 把錯誤送進 Reducer 集中處理即可。
為了讓錯誤處理更方便,我在 Express Server 上會搭配 2 個 Helpers 來運作,這些 Helpers 是在 src/server/middlewares/mountHelper.js 中掛在 res 物件上的,實際功能其實只是包裝 Dispatch Action 的動作。
res.pushError() 是用來把錯誤塞進 Error Queue 裡,Server 還不會送出 Response:
import Errors from './path/to/constants/Errors';
// ...
if (err) {
res.pushError(Errors.ODM_VALIDATION, err);
}
當所有錯誤都被 Push 進到 Error Queue,可以呼叫 res.errors() 送出 Response。
import Errors from './path/to/constants/Errors';
// ...
if (err) {
res.pushError(Errors.ODM_VALIDATION);
res.pushError(Errors.ODM_OPERATION_FAIL);
res.pushError(...);
return res.errors();
}
從這兩個例子中還可以看到,我們集中管理了 Errors 物件:
import ErrorCodes from './ErrorCodes';
export default {
// ...
[ErrorCodes.PERMISSION_DENIED]: {
code: ErrorCodes.PERMISSION_DENIED,
status: 403,
title: 'Permission Denied',
detail: 'You are not allowed to access the resource.',
},
// ...
};
前一篇教過各位以 Promise 的形式撰寫 API,但其實我刻意忽略了示範 API 的錯誤處理,怕混淆各位所以留到這篇說明。
API 的錯誤處理必須追朔到 ApiEngine 的撰寫,在 Expose 出來的 get、post、put、del 四個 Methods 回傳值都是 Promise,所以我們便能自訂什麼情況下要觸發 Promise 的錯誤,我是統一在 API Response 中使用 errors 這個 key 來儲存錯誤陣列,所以 ApiEngine 只要偵測到 res.errors.length > 0 就代表收到的 Response 發生錯誤了。
class ApiEngine {
get(path, { params, data, files } = {}) => {
return new Promise((resolve, reject) => {
// ...
request.end((err, { body } = {}) => {
// Superagent 發生內部錯誤
if (err) {
return reject(body || err);
}
// API Server 回傳自訂錯誤
if (body.errors && body.errors.length > 0) {
return reject(body.errors);
}
return resolve(body);
});
});
}
// ...
}
了解 ApiEngine 的錯誤處理機制後,就可以搭配 pushError 把它套用在實際的 API 呼叫上了:
// Server 端用法
export default {
// ...
fetchTodo: (req, res, next) => {
todoAPI(req.store.getState().apiEngine)
.read(req.params.todoId)
.catch(() => {
res.pushError(Errors.STATE_PRE_FETCHING_FAIL, {
detail: 'Cannot read todo',
});
next();
})
.then((json) => {
req.store.dispatch(setTodo(json.todo));
next();
});
},
};
// Client 端用法
componentDidMount() {
let { dispatch, apiEngine } = this.props;
todoAPI(apiEngine)
.read(9527)
.catch((err) => {
dispatch(pushErrors(err));
// 這行是必要的,否則即使錯誤發生了,一樣會繼續執行 then 的部分
throw err;
})
.then((json) => {
dispatch(setTodo(json.todo));
});
}
接下來讓我們看看 Server-Only 的錯誤該怎麼處理。
第一種狀況是 Server Process 本身發生了沒有處理到的 Error,或是 Promise 中發生例外狀況,利用 Node 本身的語法就能處理掉了,通常是直接重啟 Server,這樣才能確保可靠度。
process.on('uncaughtException', (err) => {
process.exit(1);
});
process.on('unhandledRejection', (reason, p) => {
throw reason;
});
另一種狀況是錯誤只發生在某一個 Request 上,參考 Express 文件的 Error handling 寫法即可,另外,在 Boilerplate 中額外做了一件事情,我們把錯誤處理的行為區隔為 production 與 non-production,production 的環境下最好不要暴露 Error Stack,規避安全議題;development 與 test 環境下為了 Debug,所以可以顯示 Error Stack。
app.use((err, req, res, next) => {
if (env !== 'production') {
res.status(500).send(`<pre>${err.stack}</pre>`);
} else {
res.status(500).send('Service Unavailable');
}
});
除了上述的幾種錯誤以外,Node 本身非同步的寫法必須經常處理 Callback 中的 Error,一般教學中都是直接 throw err:
doSomething((err, ...data) => {
if (err) {
throw err;
}
// ...
})
這般作法其實很粗糙,只是把 Error 送入前面提到的 Express Request Error Handler 中集中處理,這對一個 API Server 而言是明顯不合格的,為了更方便維護,還有為了在 Response 中更明確的知道錯誤發生在哪個環節,我在 Boilerplate 實做了 handleError 這個模組。
用法如下,直接把 handleError 包住原本的 Callback Function,並且消耗掉 Callback 的第一個 err 參數,所以在 Callback 中就不用再處理錯誤了。
import { handleSomeError } from '../decorators/handleError';
doSomething(handleSomeError(res)((...data) => {
// ...
}))
使用上會把 res 物件傳遞進去,如此便能在錯誤發生時,透過 handleError 模組發送 Response。此模組廣泛用在 ODM 的操作上,請看以下例子:
import { handleDbError } from '../decorators/handleError';
export default {
list(req, res) {
User.paginate({ page: req.query.page }, handleDbError(res)((page) => {
User
.find({})
.limit(page.limit)
.skip(page.skip)
.exec(handleDbError(res)((users) => {
res.json({
users: users,
page: page,
});
}));
}));
},
};
每一次的 ODM 操作都可能發生錯誤,如果沒有透過 handleError 模組來簡化,程式碼的可讀性會非常差,那絕對會是一場災難。
Client-Only 的錯誤處理只需要延用 res.pushError 使用到的 Actions 即可,只是在 Client 端沒有提供 Helper,要自己 Dispatch Actions。範例其實在上面已經出現過了,請見 API 的錯誤處理 的 Client 端使用範例。
至於錯誤的顯示方式,每個開發者、每個應用都不相同,有的人習慣用 Bootstrap 的 Alert,有的人喜歡 Popup 提醒視窗,在 Boilerplate 中,我是用前者寫成一個 ErrorList 元件,在此不贅述了,請直接參考原始碼。