JAVASCRIPT V2 · MODULES & DEBUG · 43
Module / Error / Debug:程式變大後,真正重要的是責任與 evidence
你已經會寫 function、DOM 與 async pipeline。現在進入「專案級」思考:哪些責任該分開、錯誤應在哪一層處理、stack trace 如何保留,以及如何從 symptom 找到最早違反 contract 的 boundary。
Learning outcomes
- 使用 ES modules 建立明確 dependency boundary。
- 區分 parse、reference、type、domain、network 與 HTTP failure。
- 知道何時 catch、何時 rethrow、何時包裝 error context。
- 能閱讀 stack trace,定位第一個可信的 application frame。
- 能用 last success / first failure 建立 debugging evidence chain。
1. Why now:500 行 app.js 的真正成本
// app.js
// fetch
// validation
// grading rules
// localStorage
// DOM render
// events
// error messages
// 500+ lines
問題不是檔案長本身,而是責任、dependency 與 failure boundary 全混在一起。當一個 API 欄位改名,你不知道要查 data layer、business rule 還是 render。
2. Mental model:Module 是 dependency graph 的節點
api.js
↓ returns domain data
grade.js
↓ pure business rule
app.js
↓ owns state/orchestration
view.js
↓ state → DOM
良好的 module boundary 應該讓 contract 清楚,而不是只把 500 行機械切成五個 100 行檔案。
3. Worked Example A:named export / import
// grade.js
export function gradeLabel(score) {
if (score >= 90) return "A";
if (score >= 80) return "B";
if (score >= 60) return "Pass";
return "Fail";
}
// app.js
import {
gradeLabel
} from "./grade.js";
console.log(
gradeLabel(85)
);
<script
type="module"
src="app.js"
></script>
import path、export name 與 runtime origin 都是 contract 的一部分。
4. Module scope:不是把 global 變多
// a.js
const secret = 123;
// b.js
console.log(secret);
// ReferenceError
ES module 有自己的 scope。想共享 value 就明確 export/import,而不是依賴 script loading order 製造隱形 global。
5. Worked Example B:分類 failure layer
| failure | example | 第一個 evidence |
|---|---|---|
| SyntaxError | parser 無法解析 | file + line/column |
| ReferenceError | 名稱不在 scope | identifier + stack |
| TypeError | value 不支援該 operation | actual value/type |
| Domain error | score = 130 | validation contract |
| Network error | connection failed | rejection + Network panel |
| HTTP error | 403 / 500 | status + response body |
| Logic error | 95 顯示 Pass | wrong branch / boundary |
6. Error object:保留 context,不要只丟字串
throw new Error(
"Student score must be 0..100"
);
Error object 能保留 name/message/stack。自訂 Error class 可以再加入 code、cause 或 domain metadata。
throw new Error(
"Failed to load students",
{ cause: error }
);
支援 cause 的 runtime 可用它保留原始 failure,而不是把 root cause 改寫掉。
7. Worked Example C:catch 不是越早越好
async function fetchStudents() {
const response =
await fetch("/api/students");
if (!response.ok) {
throw new Error(
"HTTP " + response.status
);
}
return response.json();
}
async function refresh() {
try {
const students =
await fetchStudents();
render(students);
} catch (error) {
showError(
"Unable to refresh"
);
console.error(error);
}
}
Data layer 負責把 protocol failure 轉成 meaningful error;UI orchestration layer 決定如何向使用者呈現。不要每層都 catch 後假裝成功。
8. Anti-pattern:吞掉 error
async function fetchStudents() {
try {
// ...
} catch (error) {
console.error(error);
return [];
}
}
呼叫端看到空陣列,會誤以為「真的沒有學生」。Failure 被偽裝成 valid data,debug 會更難。
9. Stack trace:從 symptom 回到 origin
TypeError: Cannot read
properties of undefined
at renderStudent (view.js:27)
at renderList (view.js:44)
at refresh (app.js:18)
先讀最上方 application frame:renderStudent 的哪個 assumption 失敗?再往 caller 回推資料從哪裡進來。不是看到 app.js 就直接重寫 refresh。
10. Last success / first failure
Network 200 OK ✓
JSON parse ✓
students.length === 3 ✓
normalizeStudent[1] ✗
render 未執行
Root cause 搜尋區間已經縮到 normalize boundary。這個方法比「四處加 console.log」有效,因為每個 evidence 都在切分系統。
11. Source map / breakpoint / debugger
function normalizeStudent(raw) {
debugger;
return {
name: raw.name.trim(),
score: Number(raw.score)
};
}
Breakpoint 可以直接檢查 local scope、call stack、closure 與實際 runtime value。Console 是 evidence tool,Sources 也是。
12. Async stack:錯誤可能跨 await boundary
async function boot() {
await refresh();
}
async function refresh() {
const students =
await fetchStudents();
render(students);
}
DevTools 會盡量保留 async stack context。看到 rejection 時仍要問:第一個 throw 在哪裡?哪一層只是 await / propagation?
13. Common mistakes
A. Module = 一個 class 一個檔案
錯。邊界應由責任與 dependency 決定。
B. 每層都 catch
結果是 error 被重複 log、重複包裝甚至被吞掉。
C. Error message 只有 "failed"
至少帶 operation/context;但不要把 secret/token 塞進 error。
D. 一看到最下層 stack frame 就改那裡
最下層通常只是入口;真正 throw 常在較上方 frame。
14. Guided exercise:拆 Grade Tracker
把以下責任分到最多四個 modules,並寫出 imports:
- fetch students
- validate score
- grade label
- render list
- button event
- application state
要求:說明每個 module 的 input/output contract,而不是只列檔名。
15. Independent exercise:Error propagation map
設計 api.js → normalize.js → app.js → view.js。製造一筆缺少 name 的資料,記錄:
- 最初 failure
- throw 的 error
- 哪一層 catch
- 使用者看到什麼
- Console 保留什麼 stack/cause
16. Challenge:403 到底在哪層修?
Network:
GET /api/students
403 Forbidden
Body:
{"error":"missing_scope"}
請區分:request construction、authentication、authorization、UI error state 四個 boundary。不要用「加 try/catch」當答案。
Project checkpoint:Grade Tracker v2 module graph
api.js
└─ fetchStudents()
student.js
├─ normalizeStudent()
└─ validateScore()
grade.js
└─ gradeLabel()
view.js
├─ renderStudents()
└─ renderStatus()
app.js
├─ state
├─ event wiring
└─ orchestration
最後為一次真實 fault injection 寫 Incident Notebook:Symptom → Evidence → Last success → First failure → Root cause → Fix → Regression。
17. Stage 4 串起來
| lesson | 核心問題 |
|---|---|
| 13 | 為什麼結果不是現在就有? |
| 42 | 如何表示、組合與傳播未來結果? |
| 26 | continuation 到底何時獲得執行機會? |
| 43 | 大型 async 專案如何切責任並追 failure? |
18. Vocabulary / Summary
- module boundary:責任與 dependency 的顯式邊界。
- contract:輸入、輸出、failure 的約定。
- propagation:error 沿 call/Promise chain 往外傳。
- root cause:最早導致錯誤狀態的原因。
- last success / first failure:用 evidence 縮小故障區間。
- regression:修正後防止同一 bug 再出現的驗證。
19. Further reading
Knowledge check · Mastery
- 替一個小 app 畫 module dependency graph。
- 解釋 catch/rethrow 與吞掉 error 的差別。
- 從 stack trace 找第一個可信 application frame。
- 用 last success / first failure 完成頁面下方 Evidence Diagnostic incidents。