iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
佛心分享-IT 人自學之術

出發吧!後端菜鳥:30 天的後端學習紀錄系列 第 12 篇

Day 12|讓錯誤集中處理:認識 Express Error Handler

  • 分享至 

  • xImage
  •  

前言

前面在 API Validation 中,我們已經把 Validation 抽成 Middleware,當 Request 不符合 Schema 規則時,不再由 Validation Middleware 自己回傳 Response,而是透過 next(error) 把錯誤往後傳。這樣做的好處是 Validation 可以專心負責資料驗證,至於錯誤最後要怎麼處理,則交給後面的流程。

不過問題來了,next(error) 把錯誤傳出去之後,Express 要怎麼知道接下來該怎麼處理?而且錯誤不一定只會發生在 Validation,Controller、資料庫操作或其他非同步程式也可能發生錯誤。如果每個地方都自己處理錯誤 Response,久了還是會出現大量重複的程式碼。

因此今天要接著處理前面留下來的問題:錯誤被傳出去之後,如何由 Express 統一處理。 我們會使用 Error Handling Middleware,讓 Validation、Controller 和其他地方發生的錯誤,最後都可以集中到同一個地方。

next(error) 把錯誤傳到哪裡?

先回頭看前面 Validation Middleware 的寫法:

function validateNote(req, res, next) {
  const result = noteSchema.safeParse(req.body);

  if (!result.success) {
    const issue = result.error.issues[0];

    const error = new Error(issue.message);
    error.statusCode = 400;

    return next(error);
  }

  req.body = result.data;

  next();
}

當 Validation 成功時,使用 next() 讓 Request 繼續往下一個 Middleware 或 Route;如果驗證失敗,則使用 next(error) 告訴 Express:「這裡發生錯誤了。」

這和一般的 next() 不太一樣。next() 是讓流程繼續往下走,而 next(error) 則會讓 Express 進入錯誤處理流程,接下來就會尋找 Error Handling Middleware。

所以前面的 Validation 並不是完全沒有處理錯誤,而是把「發現錯誤」和「處理錯誤」拆開。Validation 負責確認資料有沒有問題,真正的錯誤 Response 則交給後面的 Error Handler。

為什麼不要每個地方自己處理錯誤?

假設 Controller 發現 Note 不存在,最直接的寫法可能是:

if (!note) {
  return res.status(404).json({
    message: "找不到這筆 Note"
  });
}

這段程式本身沒有問題,但如果之後每支 API 都用同樣的方式處理錯誤,很容易變成每個 Controller 都在判斷 Status Code、建立錯誤訊息和產生 Response。

Validation 可能有自己的 400 Response,Controller 有自己的 404 Response,資料庫發生問題時又有另一套 500 Response。當 API 數量增加之後,不只會有大量重複程式碼,也可能讓不同 API 使用不同的錯誤格式。

因此比較好的方式,是讓發現錯誤的地方只負責把錯誤傳出去,最後由同一個 Error Handler 決定 Status Code 和 Response 格式。

Controller 發生錯誤時怎麼辦?

Controller 同樣可以把錯誤交給後面的錯誤處理流程。例如:

if (!note) {
  const error = new Error("找不到這筆 Note");
  error.statusCode = 404;

  throw error;
}

這裡沒有直接使用 res.status() 回傳 Response,而是建立一個 Error,並把 statusCode 一起放進去。這樣錯誤在往後傳遞的過程中,就可以帶著後面處理時需要的資訊。

例如我們可能會把建立 Error 的部分抽成函式:

function createError(message, statusCode) {
  const error = new Error(message);

  error.statusCode = statusCode;

  return error;
}

之後 Controller 就可以寫成:

if (!note) {
  throw createError("找不到這筆 Note", 404);
}

這裡真正重要的不是 createError() 本身,而是讓錯誤在傳遞時,同時帶著錯誤訊息和 Status Code,讓後面的 Error Handler 可以根據這些資訊建立 Response。

非同步錯誤怎麼處理?

當 API 開始接上資料庫之後,查詢資料、建立資料或更新資料通常都會變成非同步操作,這時候錯誤可能發生在 await 的過程中。

例如:

const getNote = async (req, res, next) => {
  try {
    const note = await getNoteById(req.params.id);

    if (!note) {
      throw createError("找不到這筆 Note", 404);
    }

    res.status(200).json({
      data: note
    });
  } catch (error) {
    next(error);
  }
};

如果 getNoteById() 查詢失敗,或找不到 Note 時主動 throw Error,程式就會進入 catch,最後透過 next(error) 把錯誤交給 Express。

這裡可以看到 try/catch 的作用不是讓 Controller 自己處理錯誤,而是把非同步流程中的錯誤接住,再交給統一的錯誤處理流程。實際使用 Express 時,也可以依照目前使用的 Express 版本和寫法決定是否需要在每個非同步 Controller 裡手動包 try/catch,不過核心概念是一樣的:錯誤最後應該進入同一個 Error Handler。

Error Handler 是什麼?

當 Validation、Controller 或其他非同步操作把錯誤交給 Express 之後,就需要一個地方統一接收這些錯誤。Express 的 Error Handling Middleware 和一般 Middleware 不同,它會有四個參數:

app.use((err, req, res, next) => {
  // Error Handler
});

第一個參數 err 就是前面傳過來的錯誤,因此可以從 err.statusCode 和 err.message 取得錯誤資訊,再統一建立 Response:

app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;

  res.status(statusCode).json({
    message: err.message || "Internal Server Error"
  });
});

例如 Validation 發生錯誤時設定 statusCode = 400,Error Handler 就會回傳 400;Controller 找不到 Note 時設定 statusCode = 404,就會回傳 404。如果收到的是沒有額外設定 statusCode 的一般 Error,就使用預設的 500。

這樣不管錯誤從哪裡發生,最後都可以使用相同的方式建立 Response。

Error Handler 要放在哪裡?

Error Handler 通常會放在所有 Route 和其他 Middleware 的後面:

app.use(express.json());

app.use("/notes", noteRouter);
app.use("/users", userRouter);

app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;

  res.status(statusCode).json({
    message: err.message || "Internal Server Error"
  });
});

因為前面的 Middleware 和 Route 必須先有機會處理 Request,發生錯誤後才會將錯誤交給 Error Handler,所以它通常會放在整個 Application 的後面。

整個流程就可以整理成:

Request
   ↓
Validation Middleware
   ↓
Router
   ↓
Controller
   ↓
資料庫 / 其他操作
   ↓
發生錯誤
   ↓
next(error)
   ↓
Error Handler
   ↓
Response

這樣錯誤就不需要散落在每一個 API 裡處理,而是有一個統一的錯誤入口。

Client 和 Developer 看到的錯誤可以不同

錯誤集中之後,還需要注意一件事情:Server 發生的錯誤,不代表所有資訊都應該直接回傳給 Client。

例如「找不到這筆 Note」是 API 使用者可以理解的錯誤,因此可以直接回傳;但像資料庫連線失敗、程式執行例外或內部錯誤,完整訊息通常比較適合留給開發者 Debug,而不是直接交給 Client。

因此 Error Handler 除了產生 Response,也可以負責把錯誤記錄下來:

app.use((err, req, res, next) => {
  console.error(err);

  const statusCode = err.statusCode || 500;

  res.status(statusCode).json({
    message: err.statusCode
      ? err.message
      : "Internal Server Error"
  });
});

這樣可以預期的錯誤,例如 400 或 404,可以回傳原本的錯誤訊息;沒有特別設定 Status Code 的錯誤,則只回傳一般性的 500 訊息,同時在 Server 端保留完整錯誤。實際專案中通常會進一步使用 Logging 工具,讓錯誤可以被搜尋、追蹤與監控。

Development 和 Production 可以不同

在 Development 環境中,通常希望看到完整的錯誤資訊,方便找到錯誤發生的位置;到了 Production,則應該避免把 Stack Trace 或資料庫錯誤等內部資訊直接暴露給 Client。

例如可以根據 NODE_ENV 做不同處理:

app.use((err, req, res, next) => {
  console.error(err);

  if (process.env.NODE_ENV === "development") {
    return res.status(err.statusCode || 500).json({
      message: err.message,
      stack: err.stack
    });
  }

  res.status(err.statusCode || 500).json({
    message: err.statusCode
      ? err.message
      : "Internal Server Error"
  });
});

這樣 Development 可以保留比較完整的資訊來協助 Debug,而 Production 則只回傳 Client 真正需要知道的內容,完整錯誤則留在 Server 端的 Log 中。

結論

前面的 Validation 解決的是「Request 進來之後,資料是否符合規則」;今天則接著處理「如果任何地方發生錯誤,這個錯誤要怎麼被傳遞和處理」。Validation Middleware 可以發現資料錯誤,Controller 可以發現資源不存在,資料庫操作也可能發生例外,但這些錯誤都不需要各自建立一套 Response,而是可以交給同一個 Error Handler。

整個流程可以整理成:

Request
   ↓
Validation
   ↓
Router
   ↓
Controller
   ↓
資料庫 / 其他操作
   ↓
發生錯誤
   ↓
next(error)
   ↓
Error Handler
   ├── Response → Client
   └── Log → Developer

這樣做之後,Validation 可以專心驗證資料,Controller 可以專心處理 API 邏輯,而 Error Handler 則負責統一處理錯誤 Response。錯誤可以在不同地方發生,但不需要在不同地方各自處理。


上一篇
Day 11|資料送進後端之前,還有一道檢查:認識 API Validation
下一篇
Day 13|Server 重開資料就消失?先學 SQL 基礎語法
系列文
出發吧!後端菜鳥:30 天的後端學習紀錄 共 13 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言