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 裡常見的 4xx5xx

狀態碼 可以先怎麼理解
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
(內部伺服器錯誤)

所以 404500 最大的差別,不只是數字不同。

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) }}
直播中

尚未有邦友留言

立即登入留言