iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
Vibe Coding

夢幻甜品師闖工程世界:Vibe Coding vs 專業開發的 0→1 冒險攻略系列 第 21 篇

Day 21|甜點做壞了,總不能只說「出錯了」吧?從狀態碼看懂 API 錯誤處理

  • 分享至 

  • xImage
  •  

小時候玩電腦的時候,最常看到的錯誤就是:

404 Not Found

那時候其實根本不知道 404 是什麼。

反正網站打不開、畫面怪怪的,對我來說就是:

「喔,出錯了。」

真的開始學程式之後才發現,原來這些數字不是隨便拿來代表「Error」而已。

每個 Status Code 都有自己的角色。

更有趣的是,這些角色本身就是 HTTP 用來說話的一種方式。

Backend 用它告訴 Frontend 這次 Request 發生了什麼,Frontend 再根據收到的結果,決定接下來要顯示什麼、讓使用者做什麼。

這也剛好接上昨天講的 API Contract。

成功時要怎麼回答需要約定,失敗時其實也一樣。

所以今天就來看:

一個 Request 失敗後,Backend 到底是怎麼把「發生什麼事」說清楚的?


一次 API 溝通,本來就有成功和失敗

像前面提到的 404 Not Found,其實就是一個很好的例子。

使用者想取得某個 Activity,但那個 Activity 根本不存在。

這時 Backend 回:

404 Not Found

不是因為 Server 壞掉了。

反而代表 Backend 有好好收到 Request,並且明確回答:

「你要的 Resource 不存在。」

我以前會把這種情況和真正的程式 Error 全部混在一起,但開始學 API 之後才發現:

「這次沒有成功」和「程式真的壞掉」,其實是兩件不同的事。


Backend 要先說清楚:這次是哪一種失敗?

真的拆開來看,Web API 會遇到的失敗其實很多:

失敗
│
├─ Request 資料不合法
├─ 身分驗證沒有通過
├─ 沒有操作權限
├─ 找不到 Resource
├─ 目前 Resource/系統狀態發生衝突
└─ Server 發生非預期 Error

看起來明明都是「Error」,但對 Frontend 來說,接下來要做的事卻差很多。

沒登入
→ 可能導回登入頁

沒權限
→ 顯示「你不能做這件事」

活動不存在
→ 顯示找不到內容

輸入資料不合法
→ 告訴使用者哪裡需要修改

Server 出錯
→ 顯示暫時無法使用

所以 Backend 不能只告訴 Frontend:

「失敗了。」

還得先說清楚:

這次到底是哪一類結果。

這就是 HTTP Status Code(HTTP 狀態碼) 在做的事。


狀態碼:這次是哪一類失敗?

Day19 已經看過 HTTP 狀態碼的五大類:

類別 大致代表
1xx 資訊性回應
2xx 成功
3xx 重新導向
4xx Client Error
5xx Server Error

前面終於認識了我最常看到的 404 Not Found。

那其他角色又都在說什麼呢?

接下來就來看幾個 Web API 裡常見的 4xx 和 5xx。

狀態碼 可以先怎麼理解
400 Bad Request Request 無法按照預期處理,例如輸入資料不合法
401 Unauthorized 缺少或無效的身分驗證資訊
403 Forbidden 身分已知,但沒有操作權限
404 Not Found 找不到指定 Resource
409 Conflict Request 和目前 Resource/系統狀態發生衝突
500 Internal Server Error Server 發生非預期 Error

狀態碼先告訴使用端:

這次 Request 最後屬於哪一類結果。

但只知道「是哪一類」,還不一定知道具體發生了什麼。


只有狀態碼還不夠,錯誤訊息也要講清楚

假設 Backend 只回:

404

Frontend 知道:

找不到 Resource。

但一個網站裡可能同時有:

Activity
User
Friendship
Notification

到底是哪一個找不到?

所以 Response Body 裡通常還會再放更具體的錯誤資訊:

{
  "message": "Activity not found"
}

把它放回整個錯誤回應裡看,就會更清楚:

Error Response(錯誤回應)
│
├─ Status Code:404
│
└─ Response Body
   └─ message:"Activity not found"
                └→ Error Message(錯誤訊息)

也就是說:

Status Code 負責分辨結果類型,Error Message 再補充具體發生了什麼。

但這份 Error Response 的格式本身也要固定。

如果 Backend 有時候回:

{
  "message": "Activity not found"
}

有時候又回:

{
  "error": "User not found"
}

Frontend 就得一直判斷,這次到底要讀 message 還是 error。

所以昨天 API Contract 講的事情,在失敗時一樣成立:

Error Response 的格式,也應該讓使用端可以預期。

尤其是 500,它和前面的 4xx 最大的差別,不只是代碼不同,而是 Backend 可能根本沒有預料到這個 Error 會發生。


500:這次是真的出問題了?

前面看到的很多失敗,其實都是 Backend 本來就知道可能發生的情況。

先把它們分成兩邊:

失敗
│
├─ 預期失敗
│  → 系統本來就知道這種情況可能發生
│
└─ 非預期 Error
   → 程式執行途中真的出了問題

像「Activity 不存在」,就是預期失敗。

Backend 知道這種情況可能發生,也知道該怎麼回答:

找不到 Activity
→ 回 404

但如果遇到的是:

Database 連線突然失敗
程式讀到不該是 undefined 的值
外部服務突然拋出例外

就比較像是:

「這個真的不是原本預期會發生的。」

這類非預期 Error,最後通常會回:

500 Internal Server Error
(內部伺服器錯誤)

所以 404 和 500 最大的差別,不只是數字不同。

404
→ Backend 知道發生了什麼
→ 也知道該怎麼回答

500
→ Server 遇到了非預期 Error

但非預期 Error 不會自己突然變成一個 500 Response。

它在 Code 裡發生之後,還得先解決另一個問題:

誰把它接住?怎麼往外傳?最後又由誰把它變成 HTTP Response?


後端錯誤處理:Error 發生之後,怎麼一路被處理?

前面知道了,非預期 Error 不會自己變成一個 500 Response。

從 Code 裡發生 Error,到最後 Frontend 收到 HTTP Response,中間其實是一整套後端錯誤處理(Backend Error Handling)的流程。

先看整條路:

HTTP Request(HTTP 請求)
↓
Route(路由)
↓
Controller / Service(控制器/服務層)
↓
發生 Error
↓
Error 被接住、往外傳
↓
Error Handler(錯誤處理器)
↓
Status Code + Error Response
(狀態碼+錯誤回應)
↓
Frontend(前端)

把中間這段 Error 被接住、往外傳 放大來看的話會長這樣:

throw
→ Code 把 Error 往外拋

try/catch
→ 外層把 Error 接住

next(error)
→ 把 Error 交給 Express

Error Handler
→ 集中處理 Error,轉成 Response

接下來,就從 throw 開始,看這顆 Error 是怎麼一路被傳下去的。


throw:先把 Error 往外拋

假設 Service 執行到一半,發現拿到的資料出現不該發生的狀況:

async function findActivity(id) {
  const activity = await db.activity.findUnique({
    where: { id }
  });

  if (activity && !activity.ownerId) {
    throw new Error('Activity data is incomplete');
  }

  return activity;
}

看到這裡的:

throw new Error(...)

可以先把它理解成:

這一層發現 Error,先把它往目前呼叫它的外層拋。

Error 被拋出去之後,如果外層有 try/catch,就可以在那裡把它接住。


try/catch:把 Error 接住

前面把 Error throw 出去之後,如果外層有 try/catch,就可以在這裡接住它。

例如 Controller 呼叫剛剛的 Service:

try {
  const activity = await findActivity(id);
} catch (error) {
  next(error);
}

如果 findActivity() 裡拋出 Error,程式就會進到 catch(error)。

這段流程可以先看成:

Service(服務層)
↓
throw Error(拋出錯誤)
↓
Controller(控制器)
↓
catch(error)(接住錯誤)

不過接住 Error,不代表一定要在這一層決定:

最後回什麼 Status Code?Error Response 又要長什麼樣?

如果不打算在這裡處理完,就可以再把 Error 往後交。


next(error):把 Error 交給 Express

在這個 Express 的寫法裡,就會看到:

next(error);

可以先把它理解成:

目前這一層不把 Error 處理完,而是交進 Express 後面的錯誤處理流程。

於是剛剛那條路又多了一步:

Service
↓
throw Error
↓
Controller
↓
catch(error)
↓
next(error)
↓
Error Handler
↓
HTTP Response

到了這裡,Error 已經被往後交出去,接下來就要有人負責把它整理成最後的 Response。


Error Handler:不同地方的 Error,可以走向同一個出口

如果每個 Controller 都各自決定:

Error 怎麼記錄?
回哪個 Status Code?
Error Response 長什麼樣?

Endpoint 一多,處理方式就很容易越來越散。

所以可以設一個共同的 Error Handler(錯誤處理器),讓沒有在前面處理完的 Error 最後走到同一個地方:

活動 Controller ─┐
好友 Controller ─┼─→ Error Handler
登入 Controller ─┘
                  ↓
            HTTP Response

概念上可能會看到這樣的寫法:

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

  res.status(500).json({
    message: 'Server error'
  });
});

這個例子先示範最簡單的情況:收到 Error、記錄下來,再回一個 500 Response。

但 Error Handler 真正負責的,不只是:

「全部回 500。」

而是把走到這裡的 Error 記錄、判斷,再轉成適合的 HTTP Response。


原來「失敗」也要被好好設計

那這三天的 API,就先走到這裡啦!

從怎麼說話、怎麼把規則定清楚,一路看到今天連「失敗」都要怎麼回答。

以前做功能時,我比較容易只看:

「正常情況有沒有跑成功?」

Error 出現了,就想辦法把它修掉。

但這次一路拆完才發現,對使用者來說,「失敗」其實也需要被好好設計。

不然他可能遇到的是突然一片空白、看不懂的錯誤訊息,甚至根本不知道現在發生了什麼。

所以前後端在對 API 時,不只成功會回什麼要講清楚,失敗時要怎麼回答,也需要一起對齊。

我現在會覺得,一個功能不是成功那條路跑得通就算做完了。

失敗的時候,系統能不能好好接住,也很重要。

而下一篇,還會碰到另一種「不能做」。

不是資料錯、不是權限問題,也不是 Server 壞掉,而是:

系統現在這個狀態,本來就不允許做這件事。

下一篇,就來看 State Machine(狀態機)。


上一篇
Day 20|同一張訂單,前台後場怎麼可以各自解讀?從 API 契約看懂 API 文件
下一篇
Day 22|流程一多腦袋就打結?從 State Machine 看懂系統的狀態怎麼走
系列文
夢幻甜品師闖工程世界:Vibe Coding vs 專業開發的 0→1 冒險攻略 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言