← JavaScript Learning Path

JAVASCRIPT V2 · MODULES & DEBUG · 43

Module / Error / Debug:程式變大後,真正重要的是責任與 evidence

你已經會寫 function、DOM 與 async pipeline。現在進入「專案級」思考:哪些責任該分開、錯誤應在哪一層處理、stack trace 如何保留,以及如何從 symptom 找到最早違反 contract 的 boundary。

Learning outcomes

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

failureexample第一個 evidence
SyntaxErrorparser 無法解析file + line/column
ReferenceError名稱不在 scopeidentifier + stack
TypeErrorvalue 不支援該 operationactual value/type
Domain errorscore = 130validation contract
Network errorconnection failedrejection + Network panel
HTTP error403 / 500status + response body
Logic error95 顯示 Passwrong 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:

要求:說明每個 module 的 input/output contract,而不是只列檔名。

15. Independent exercise:Error propagation map

設計 api.js → normalize.js → app.js → view.js。製造一筆缺少 name 的資料,記錄:

  1. 最初 failure
  2. throw 的 error
  3. 哪一層 catch
  4. 使用者看到什麼
  5. 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如何表示、組合與傳播未來結果?
26continuation 到底何時獲得執行機會?
43大型 async 專案如何切責任並追 failure?

18. Vocabulary / Summary

19. Further reading

Knowledge check · Mastery

  1. 替一個小 app 畫 module dependency graph。
  2. 解釋 catch/rethrow 與吞掉 error 的差別。
  3. 從 stack trace 找第一個可信 application frame。
  4. 用 last success / first failure 完成頁面下方 Evidence Diagnostic incidents。