做到 Day 17 之後,這個專案已經不只是幾個簡單頁面。
目前已經有:
新增任務
查看任務
編輯任務
刪除任務
快速修改狀態
狀態篩選
分類篩選
MySQL
功能變多之後,下一個一定會遇到的問題就是:
程式出錯時,我到底要先看哪裡?
所以 Day 18 不新增功能,也不重構。
今天只做一件事:
練習看懂錯誤、找到錯誤位置,再判斷要不要修。
今天主要看這幾種情況:
404 Not Found
400 Bad Request
500 Internal Server Error
Traceback
ValueError
資料庫連線錯誤
今天不做:
分類 CRUD
搜尋
分頁
services/
routes/
Claude API
也不會因為看到錯誤就立刻叫 Claude Code 全部重寫。
我先把目前專案狀態交給 Claude Code:
目前 Task CRUD、MySQL 儲存、篩選都已經完成。
今天不加功能、不重構、不建立 services/routes。
目的是練習除錯流程。
請針對幾種常見錯誤情境說明:
1. 怎麼觸發
2. 瀏覽器會看到什麼
3. 終端機會看到什麼
4. 有沒有 Traceback
5. Traceback 要先看哪裡
6. 目前程式是已經處理,還是會直接噴 500
先分析,不要修改任何檔案。
最後決定先測:
1. 找不到 task_id → 404
2. 非法 status → 400
3. 日期格式錯誤 → 500
4. MySQL 執行中斷線 → 500
目前編輯、刪除、更新狀態都有:
Task.query.get_or_404(task_id)
所以可以直接測:
http://127.0.0.1:5000/tasks/9999/edit
假設:
id = 9999
不存在,Flask 就會回:
404 Not Found
實測結果:
HTTP Status:
404
瀏覽器:
Flask 預設 Not Found 頁面
終端機:
只有 access log
沒有完整 Traceback
例如:
GET /tasks/9999/edit HTTP/1.1" 404
一開始我以為只要是錯誤都會有 Traceback。
但這次沒有。
原因是 404 是程式預期中的 HTTP 錯誤回應。
get_or_404()
已經把「找不到資料」轉成 Flask 可以正常回應的 404。
所以可以先這樣理解:
404
→ 要找的資源不存在
→ Flask 已經正常處理
→ 通常不會出現未處理例外的 Traceback
Day 16 的狀態更新有:
new_status = request.form.get("status")
if new_status not in Task.STATUS_LABELS:
abort(400)
目前合法值只有:
todo
in_progress
done
正常首頁不會送其他內容。
所以我用 Postman 故意送:
status = bogus
Method:
POST
網址:
http://127.0.0.1:5000/tasks/2/status
Body 選:
x-www-form-urlencoded
內容:
KEY VALUE
status bogus
結果:
400 Bad Request
實測結果:
HTTP Status:
400
瀏覽器 / Postman:
Bad Request
終端機:
只有 access log
沒有完整 Traceback
例如:
POST /tasks/2/status HTTP/1.1" 400
這跟 404 很像。
因為:
abort(400)
也是程式刻意回傳的 HTTP 錯誤。
所以:
400 / 404
不一定代表程式壞掉。
有時候反而代表:
後端驗證有正常作用
目前新增任務和編輯任務都有:
due_date = (
datetime.strptime(due_date_text, "%Y-%m-%d").date()
if due_date_text
else None
)
正常瀏覽器使用:
<input type="date">
通常會送:
2026-10-02
所以一般操作不容易出錯。
但如果不用瀏覽器,直接用工具送:
due_date = not-a-date
就會打到:
datetime.strptime(...)
這次真的出現:
500 Internal Server Error
跟前面的 404、400 不一樣。
瀏覽器看到:
Werkzeug 互動式除錯頁面
終端機則出現:
完整 Python Traceback
這才是典型的「程式沒有處理住的例外」。
這次最後幾行大概是:
File "app.py", line 57, in new_task
datetime.strptime(due_date_text, "%Y-%m-%d").date()
ValueError: time data 'not-a-date' does not match format '%Y-%m-%d'
我現在會從最下面開始看。
先看:
ValueError
再看:
time data 'not-a-date' does not match format '%Y-%m-%d'
就知道問題是:
日期格式不符合
再往上找第一個:
app.py
就能知道是哪一行自己的程式造成的。
Traceback 上面通常會有很多:
Flask
Werkzeug
SQLAlchemy
Python 內部套件
第一次看很容易被嚇到。
但真正有用的通常是:
最後一行
→ 錯誤類型和原因
往上第一個自己專案的檔案
→ 錯誤發生位置
這次就是:
ValueError
↓
app.py
↓
datetime.strptime(...)
所以可以很快排除:
不是 CSS
不是 Jinja2
不是 MySQL
而是日期轉換。
第一次用 curl 測日期格式時,因為 Windows 終端機中文編碼的關係,送出去的資料有問題。
結果意外建立了一筆:
title = ""
的任務。
測試資料後來已經刪掉。
但這件事反而讓我發現一個真實問題。
新增任務頁目前有:
<input type="text" name="title" required>
所以正常用瀏覽器時,標題空白會被擋住。
但:
required
只是在瀏覽器端驗證。
如果直接用:
Postman
curl
Python
送 POST,就可以繞過。
目前後端是:
title=request.form.get("title", "")
並沒有再檢查:
title 是否為空
所以仍然可能把空標題存進資料庫。
比較完整的後端驗證未來可以像:
title = request.form.get("title", "").strip()
if not title:
abort(400)
但 Day 18 的目標是:
學會發現問題
學會看 Traceback
不是開始補表單驗證功能。
所以今天先記下:
額外發現:
後端尚未驗證 title 不可為空
之後再處理。
資料庫錯誤又是另一種類型。
目前 app.py 啟動時有:
try:
with app.app_context():
db.session.execute(text("SELECT 1"))
print("MySQL 連線成功")
except Exception as e:
print("MySQL 連線失敗:", e)
所以啟動檢查失敗時,終端機會看到:
MySQL 連線失敗:...
要注意:
app.run(debug=True)
還是在 try/except 後面。
所以即使:
SELECT 1
失敗,Flask 伺服器還是可能繼續啟動。
之後只要進入需要查資料庫的頁面,例如:
/
還是可能再次遇到真正的資料庫例外。
另一種情況是:
Flask 已經正常啟動
↓
MySQL 突然停止
↓
重新整理首頁
這時 index() 會執行:
Task.query
但資料庫已經連不上。
可能看到:
sqlalchemy.exc.OperationalError
或:
pymysql.err.OperationalError
瀏覽器會是:
500
終端機也會有完整 Traceback。
看到 Traceback 時,一樣從最下面往上看。
如果看到:
OperationalError
pymysql
sqlalchemy
就應該先想到:
資料庫連線
而不是跑去改:
index.html
CSS
Jinja2
可以先檢查:
MySQL80 有沒有啟動
DB_HOST
DB_PORT
DB_NAME
DB_USER
DB_PASSWORD
今天實際測試後,可以先整理成:
404
→ 找不到資源
→ 預期中的 HTTP Response
→ 通常沒有完整 Traceback
400
→ 請求內容不合法
→ 預期中的 HTTP Response
→ 通常沒有完整 Traceback
500
→ 程式執行時發生未處理例外
→ 瀏覽器看到錯誤頁
→ 終端機看到完整 Traceback
這三種不能混在一起看。
以前看到一大串紅字,很容易從第一行開始慢慢讀。
Day 18 之後,我會先:
1. 看最下面
↓
2. 找錯誤類型
↓
3. 看錯誤訊息
↓
4. 往上找第一個自己的檔案
↓
5. 看那一行程式
例如這次:
ValueError
↓
not-a-date
↓
app.py
↓
datetime.strptime(...)
問題範圍一下就縮小很多。
以前可能會說:
壞掉了,幫我修。
但這樣 Claude 很可能只能自己猜。
現在比較好的方式是:
我做了什麼
我預期什麼
實際發生什麼
錯誤訊息是什麼
一起給它。
例如:
我對 /tasks/new 送出 POST。
due_date 使用 not-a-date。
預期:
後端不要直接噴 500。
實際:
終端機出現:
ValueError: time data 'not-a-date'
does not match format '%Y-%m-%d'
請先分析:
1. 錯誤類型
2. 發生位置
3. 發生原因
4. 最小修改方式
先不要修改程式。
這樣我自己也可以一起理解。
如果真正問題只是:
日期格式
結果 Claude 想一次改:
app.py
models/task.py
schema.sql
templates/
那就很可疑。
一個小 bug 突然牽涉很多檔案,有可能是在:
順便重構
所以我現在會先問:
這個問題最小修改範圍是什麼?
確認後才讓它改。
就算 Claude Code 最後說:
已修復
也不代表真的完成。
還是要重新走一次:
原本會出錯的操作
例如:
POST 錯誤日期
如果真的有修,就重新測。
還要確認:
原本正常功能沒有被改壞
所以完整流程應該是:
重現
↓
分析
↓
修改
↓
重新重現
↓
確認修好
今天最後把自己的流程整理成:
1. 先重現問題
↓
2. 記錄剛剛做了什麼
↓
3. 看 HTTP Status
↓
4. 看瀏覽器錯誤頁
↓
5. 看終端機 Traceback
↓
6. 從最後一行找錯誤類型
↓
7. 往上找第一個自己的檔案
↓
8. 判斷問題在哪一層
↓
9. 再交給 Claude Code 分析
↓
10. 用最小範圍修正
↓
11. 重新做原本操作驗證
這比:
出錯
↓
直接叫 AI 修
更可靠。
這次已經實際確認:
找不到 task_id
→ 404
→ 無完整 Traceback
非法 status
→ 400
→ 無完整 Traceback
錯誤日期格式
→ 500
→ ValueError
→ 有完整 Traceback
也理解了資料庫連線錯誤時,要從:
OperationalError
PyMySQL
SQLAlchemy
這類關鍵字去判斷問題。
今天沒有增加任何新功能。
但我現在開始比較能分辨:
這是預期中的 HTTP 錯誤
還是:
這是真的程式例外
也開始知道看到一大串 Traceback 時,不需要全部從頭看。
先找:
錯誤類型
錯誤訊息
自己的檔案
出錯行數
就能把問題範圍縮小。
而 Claude Code 在除錯時最有用的地方,也不是直接替我亂改,而是幫我:
讀錯誤訊息
縮小範圍
解釋原因
提出最小修正
Day 18 到這裡完成。
下一步會繼續練習另一件更重要的事:
Claude Code 說「修好了」,我要怎麼知道它真的修好了?