Lesson 1.5.4 — Exception & Stack Unwinding
0. Metadata (Thông tin bài học)
| Thuộc tính | Giá trị |
|---|---|
| Stage | 1 — JavaScript Execution Model |
| Module | 1.5 — Call Stack & Execution Tracing |
| Lesson | 1.5.4 — Exception & Stack Unwinding |
| Competency | C02 — JavaScript Runtime (C02.6 Call Stack) |
| Depth Target | L3–L4 (Use / Debug) |
| Prerequisites | Error Handling cơ bản (Stage 0), Stack Frames (Lesson 1.5.1), Nested Calls (Lesson 1.5.2), Recursion & stack growth (Lesson 1.5.3) |
| Cognitive Load | Medium–High |
1. Why This Exists (Vì sao cần học)
Bạn đã biết một nested call bình thường quay về caller bằng return hoặc bằng cách đi đến cuối function. Nhưng exception tạo ra một control flow khác.
Xem đoạn code sau:
function a() {
console.log("a:start");
b();
console.log("a:end");
}
function b() {
console.log("b:start");
throw new Error("boom");
}
a();b() không return về a() theo đường bình thường. Sau throw, dòng tiếp theo trong b() không chạy. Nếu a() không có catch, dòng console.log("a:end") cũng không chạy.
Câu hỏi quan trọng không còn chỉ là "function nào return trước?" mà là: exception đi qua Call Stack như thế nào, frame nào bị bỏ khỏi stack, và ở đâu normal execution được phép tiếp tục?
Learning Goal (Mục tiêu học)
Bài này dùng Call Stack mental model để giải thích exception propagation và stack unwinding. Mục tiêu không phải học toàn bộ error-handling architecture, mà là trace được một synchronous exception từ nơi throw xảy ra đến catch boundary hoặc đến trạng thái unhandled.
2. Prerequisites (Yêu cầu đầu vào)
Trước khi học bài này, bạn cần:
- [ ] Biết
throwtạo ra abrupt completion thay vì normal completion. - [ ] Biết cú pháp cơ bản của
try...catch. - [ ] Vẽ được Call Stack theo mô hình
push → execute → pop. - [ ] Trace được caller → callee → resume trong nested calls.
- [ ] Phân biệt được
returnvalue với control flow. - [ ] Biết mỗi invocation có frame/execution state riêng.
Nếu bạn vẫn chưa trace chắc được a() → b() → c() → return → resume, quay lại Lesson 1.5.2 — Nested Calls trước.
3. Learning Objectives (Mục tiêu học tập)
Sau bài này, bạn có thể:
- Trace một synchronous exception qua chuỗi nested calls 3–4 tầng.
- Giải thích stack unwinding bằng frame lifecycle thay vì chỉ nói "error đi lên trên".
- Xác định statement nào bị bỏ qua sau khi
throwxảy ra. - Phân biệt normal return flow với exception propagation.
- Xác định
catchboundary gần nhất trong dynamic call chain. - Dự đoán Call Stack còn lại khi exception được bắt ở một caller phía trên.
- Debug một case exception bị swallow hoặc bị catch ở sai boundary.
- Giải thích vì sao stack trace thường hữu ích để reconstruct caller chain tại thời điểm lỗi.
4. Mental Model (Mô hình tư duy)
Với normal call flow, caller tạm dừng ở call site, callee chạy, rồi control quay lại caller:
caller
↓ call
callee
↓ normal completion
caller resumes after call siteVới exception, nếu invocation hiện tại không xử lý exception bên trong chính nó, normal execution của invocation đó bị hủy và propagation tiếp tục ra caller:
throw
↓
current execution path stops
↓
no catch in current invocation
↓
current invocation exits abruptly
↓
frame removed
↓
propagate to caller
↓
repeat until catch boundary or unhandledMột catch boundary thay đổi hướng control flow:
throw in c()
↓
c() has no catch
↓
c() exits abruptly
↓
b() has no catch
↓
b() exits abruptly
↓
a() has active catch around the call path
↓
control enters catch in a()
↓
a() may continue after try...catchMental Model (Mô hình tư duy)
Stack unwinding trong lesson này là quá trình loại bỏ các active invocation không xử lý được exception khi exception propagate ra ngoài. Không phải mọi throw đều lập tức pop mọi frame: nếu exception được catch ngay trong cùng invocation, frame đó vẫn đang active và control chuyển sang catch trong chính invocation đó.
Ba câu hỏi phải luôn hỏi khi trace exception
Tại mỗi tầng của Call Stack, hỏi:
- Exception đang ở invocation nào?
- Invocation này có active
catchboundary bao quanh execution path hiện tại không? - Nếu không có, invocation nào sẽ bị kết thúc và caller nào nhận propagation tiếp theo?
Nếu trả lời được ba câu này, stack unwinding trở thành một trace cơ học thay vì "error nhảy lung tung".
5. Core Concepts (Các khái niệm cốt lõi)
Essential (Bắt buộc)
| Khái niệm | Ý nghĩa trong lesson |
|---|---|
| Exception | Một abrupt completion do throw tạo ra trong execution path hiện tại. |
| Throw site | Vị trí nơi throw được thực thi. |
| Exception propagation | Quá trình exception tiếp tục ra caller khi invocation hiện tại không xử lý nó. |
| Stack unwinding | Quá trình các invocation không xử lý exception kết thúc đột ngột và frame của chúng rời Call Stack. |
| Catch boundary | catch đang active và bao quanh execution path nơi exception propagate tới. |
| Handled exception | Exception đã được một catch nhận, cho phép control tiếp tục theo flow của catch/sau try...catch. |
| Unhandled exception | Exception đi hết synchronous call chain mà không gặp catch phù hợp trong execution path. |
Supporting (Hỗ trợ)
throwdừng normal execution của path hiện tại ngay tại throw site.- Statement sau
throwtrong cùng path không chạy trừ khi control flow đi vào một handler khác rồi quay tới một vùng code khác. - Caller chỉ resume bình thường sau call site nếu callee hoàn tất bình thường.
- Nếu callee hoàn tất bằng exception và caller không catch, caller cũng không resume theo đường normal sau call site.
- Frame chứa
catchboundary có thể vẫn còn active; các frame bên trên nó mới là phần bị unwound. - Sau khi
catchxử lý xong, function chứacatchcó thể tiếp tục chạy các statement phía sautry...catchnếu không có abrupt completion mới.
Awareness (Biết tồn tại)
catchcó thểthrowlại exception, khiến propagation tiếp tục ra caller phía trên.finallytham gia vào control flow khi rờitry/catch, nhưng precedence chi tiết giữareturn,throwvàfinallykhông phải mục tiêu của bài này.- Formatting của stack trace có thể khác giữa browser/runtime, nhưng caller chain vẫn là một signal quan trọng khi debug.
- Exception trong callback chạy ở một thời điểm async khác không còn nằm trên synchronous call chain ban đầu; nội dung này thuộc Stage 3.
Out of Scope (Không học trong bài này)
- Promise rejection,
async/await, task, microtask, Event Loop — Stage 3. - Error taxonomy production-scale, retry policy, fallback strategy — các Stage sau.
- React Error Boundary — Stage 8.
finallyprecedence nâng cao.- Engine-specific exception tables, native stack implementation, JIT deoptimization.
- Source maps và distributed tracing.
6. Worked Example (Ví dụ phân tích từng bước)
Ta dùng đúng pattern canonical của Stage 1, nhưng thêm catch ở caller ngoài cùng để quan sát boundary:
function a() {
console.log("a:start");
try {
b();
console.log("a:after-b");
} catch (error) {
console.log("a:caught", error.message);
}
console.log("a:end");
}
function b() {
console.log("b:start");
c();
console.log("b:end");
}
function c() {
console.log("c:start");
throw new Error("boom");
}
a();Step 1 — a() được gọi
a() in a:start, sau đó vào try và gọi b().
Call Stack:
Global
└── a()
└── b()Frame a() vẫn active vì b() chưa hoàn tất.
Step 2 — b() gọi c()
b() in b:start, rồi gọi c().
Call Stack:
Global
└── a()
└── b()
└── c()b() đang chờ c() hoàn tất. Dòng console.log("b:end") chưa được thực thi.
Step 3 — c() thực thi throw
c() in c:start, sau đó chạy throw new Error("boom");.
c() không có catch bao quanh throw site. Vì vậy c() không normal-return về b(). Invocation c() kết thúc đột ngột và frame c() rời stack.
Stack chuyển từ:
Global
└── a()
└── b()
└── c()thành:
Global
└── a()
└── b()Exception tiếp tục propagate tới b().
Step 4 — b() cũng không có catch
b() không có handler cho path đang chạy. Vì vậy b() cũng kết thúc đột ngột.
Điều này có nghĩa console.log("b:end") không chạy.
Frame b() rời stack:
Global
└── a()Exception tiếp tục propagate tới invocation a().
Step 5 — a() có active catch boundary
Lời gọi b() nằm bên trong try của a(). Khi exception propagate trở lại a(), control không tiếp tục tại statement ngay sau b();.
Vì vậy console.log("a:after-b") không chạy.
Thay vào đó, control chuyển vào catch, nên a() in a:caught boom.
Điểm quan trọng: frame a() không bị pop trước khi catch chạy. Chính invocation a() chứa catch boundary đang xử lý exception.
Step 6 — Sau catch, normal flow của a() tiếp tục
Sau khi catch hoàn tất và không throw thêm exception, a() tiếp tục tới console.log("a:end"), rồi kết thúc bình thường.
Output hoàn chỉnh là:
a:start
b:start
c:start
a:caught boom
a:endKhông có b:end và không có a:after-b.
Step 7 — Tóm tắt stack transition
PUSH:
Global
Global → a()
Global → a() → b()
Global → a() → b() → c()
THROW in c():
Global → a() → b() → c()
↑ throw
UNWIND:
c() exits abruptly
Global → a() → b()
b() has no catch
b() exits abruptly
Global → a()
HANDLE:
a() catch receives exception
Global → a()
NORMAL COMPLETION:
a() finishes
GlobalQuan sát quan trọng
Stack unwinding không có nghĩa chương trình luôn dừng hoàn toàn. Nó chỉ loại bỏ các invocation mà exception đi xuyên qua. Nếu gặp catch boundary, execution có thể tiếp tục từ handler đó.
7. Prediction Exercise (Bài tập dự đoán)
Bài 1 — Dòng nào chạy?
Đừng chạy code. Hãy ghi chính xác output và đánh dấu function nào bị unwound.
function first() {
console.log("first:start");
second();
console.log("first:end");
}
function second() {
console.log("second:start");
third();
console.log("second:end");
}
function third() {
console.log("third:start");
throw new Error("stop");
}
try {
first();
} catch (error) {
console.log("caught");
}Đáp án & Giải thích
Output:
first:start
second:start
third:start
caughtthird() throw và không catch nên third() kết thúc đột ngột. Exception propagate sang second(), khiến second() bị unwound trước khi second:end chạy. Sau đó first() cũng bị unwound trước first:end. catch ở Global-level try...catch nhận exception và in caught.
Bài 2 — Catch ở giữa stack
Đừng chạy code. Hãy dự đoán output và Call Stack ngay khi catch của middle() bắt đầu chạy.
function outer() {
console.log("outer:start");
middle();
console.log("outer:end");
}
function middle() {
console.log("middle:start");
try {
inner();
} catch (error) {
console.log("middle:caught");
}
console.log("middle:end");
}
function inner() {
throw new Error("boom");
}
outer();Đáp án & Giải thích
Output là:
outer:start
middle:start
middle:caught
middle:end
outer:endKhi middle() bắt đầu chạy catch, frame inner() đã bị unwound nhưng frame middle() vẫn active vì chính middle() chứa handler. Call Stack lúc đó:
Global
└── outer()
└── middle() ← đang chạy catchSau catch, middle() tiếp tục normal flow với middle:end, return về outer(), rồi outer() resume và in outer:end.
Bài 3 — Throw ngay trong cùng invocation có catch
function run() {
try {
console.log("before");
throw new Error("boom");
console.log("unreachable");
} catch (error) {
console.log("caught");
}
console.log("after");
}
run();Câu hỏi: Có bao nhiêu function frame bị unwound trước khi catch chạy?
Đáp án & Giải thích
Không có function frame nào cần bị pop trước khi handler chạy. Exception được throw và catch ngay trong cùng invocation run().
Output là:
before
caught
afterconsole.log("unreachable") không chạy vì normal path trong try đã bị cắt tại throw.
8. Implementation Lab (Bài lab thực hành)
Lab 1 — Guided: thêm error boundary đúng tầng
Cho code:
function parsePort(raw) {
const port = Number(raw);
if (!Number.isInteger(port) || port <= 0) {
throw new Error("Invalid port");
}
return port;
}
function loadConfig(rawPort) {
return {
port: parsePort(rawPort),
};
}Yêu cầu: viết startApp(rawPort) sao cho:
- gọi
loadConfig(rawPort); - nếu thành công, in
starting <port>; - nếu parsing throw,
startAppcatch và inconfig error; - exception không propagate ra ngoài
startApptrong lab này.
Đáp án tham khảo
function startApp(rawPort) {
try {
const config = loadConfig(rawPort);
console.log("starting", config.port);
} catch (error) {
console.log("config error");
}
}Với startApp("abc"), parsePort() throw. parsePort() bị unwound, sau đó loadConfig() cũng bị unwound. startApp() chứa catch boundary nên frame startApp() vẫn active và control chuyển sang catch.
Lab 2 — Partial Scaffold: trace trước khi sửa
function readUser(user) {
return readProfile(user.profile);
}
function readProfile(profile) {
return readName(profile.name);
}
function readName(name) {
if (!name) {
throw new Error("Missing name");
}
return name;
}
function renderUser(user) {
// TODO: add boundary here
const name = readUser(user);
console.log("user:", name);
}Yêu cầu:
- Với
renderUser({ profile: {} }), vẽ stack tại throw site. - Ghi thứ tự frame bị unwound nếu chưa sửa code.
- Thêm boundary vào
renderUserđể inCannot render userthay vì để exception đi ra ngoài. - Sau khi sửa, chỉ ra frame nào không bị unwound trước khi handler chạy.
Đáp án tham khảo
Tại throw site:
Global
└── renderUser()
└── readUser()
└── readProfile()
└── readName() ← throwNếu chưa có catch, exception lần lượt đi qua readName() → readProfile() → readUser() → renderUser() rồi tiếp tục ra caller phía ngoài.
Một cách thêm boundary:
function renderUser(user) {
try {
const name = readUser(user);
console.log("user:", name);
} catch (error) {
console.log("Cannot render user");
}
}Khi handler chạy, readName, readProfile và readUser đã rời stack; renderUser vẫn active vì chính invocation này chứa catch boundary.
Lab 3 — Independent: rethrow có chủ đích
Viết loadUser() dựa trên API giả lập sau:
function fetchUserFromCache() {
throw new Error("Cache unavailable");
}Yêu cầu:
loadUser()catch lỗi để logcache failed;- sau khi log, exception phải tiếp tục propagate ra caller;
main()là boundary ngoài cùng và inmain recovered.
Đáp án tham khảo
function loadUser() {
try {
return fetchUserFromCache();
} catch (error) {
console.log("cache failed");
throw error;
}
}
function main() {
try {
loadUser();
} catch (error) {
console.log("main recovered");
}
}
main();loadUser() có catch nhưng không biến exception thành handled completion cuối cùng; nó throw error lại. Vì vậy propagation tiếp tục tới main().
9. Edge Cases (Các trường hợp ngoại lệ)
Edge Case 1 — Catch không đồng nghĩa caller phía dưới resume tại call site
function outer() {
try {
inner();
console.log("after inner");
} catch (error) {
console.log("caught");
}
}
function inner() {
throw new Error("boom");
}Khi inner() throw, outer() không resume tại console.log("after inner"). Control chuyển thẳng tới catch.
Common Misconception (Nhầm lẫn phổ biến)
"Exception được catch nên code ngay sau call site vẫn chạy" là sai. Handler thay đổi control flow. Các statement còn lại trong phần try sau throw path bị bỏ qua.
Edge Case 2 — Catch rồi im lặng có thể làm mất signal lỗi
function loadConfig() {
try {
return parseConfig();
} catch (error) {
console.log("failed");
}
}function loadConfig() {
try {
return parseConfig();
} catch (error) {
console.log("failed");
throw error;
}
}Hai phiên bản có contract khác nhau. Phiên bản Swallow biến path lỗi thành normal completion của loadConfig() với kết quả undefined nếu không return giá trị khác. Phiên bản Rethrow giữ exception flow để caller phía trên quyết định xử lý.
WARNING
Không có quy tắc "luôn rethrow" hoặc "luôn catch". Điều cần trace là boundary này đang biến đổi control contract như thế nào và caller phía trên đang kỳ vọng behavior gì.
Edge Case 3 — Throw trực tiếp trong cùng frame không cần unwind caller
Nếu try và catch nằm trong cùng invocation chứa throw site, exception có thể được xử lý mà không pop function frame đó. Vì vậy câu "mỗi lần throw thì current frame luôn bị pop" là over-simplification sai.
Edge Case 4 — Stack trace format không phải contract cố định
Tên function, file path, line/column và cách internal frames được hiển thị có thể khác giữa runtime/tooling. Khi debug, ưu tiên invariant có giá trị hơn: thứ tự caller chain và throw origin mà trace đang cung cấp, sau đó đối chiếu source thực tế.
10. Debug Lab (Bài lab gỡ lỗi)
Symptom (Triệu chứng)
UI gọi getDisplayName() nhưng đôi lúc cả action bị dừng và dòng render complete không xuất hiện.
Reproduction (Tái hiện lỗi)
function normalizeUser(user) {
if (!user) {
throw new Error("User required");
}
return user;
}
function getDisplayName(user) {
const normalized = normalizeUser(user);
return normalized.name ?? "Anonymous";
}
function render(user) {
console.log("render:start");
const name = getDisplayName(user);
console.log("name:", name);
console.log("render complete");
}
render(null);Evidence (Bằng chứng)
render:startxuất hiện.name:không xuất hiện.render completekhông xuất hiện.- Throw origin nằm trong
normalizeUser(). - Caller chain là
render()→getDisplayName()→normalizeUser().
Hypothesis (Giả thuyết)
normalizeUser() throw, getDisplayName() không có handler, và render() cũng không có handler. Vì vậy exception unwind qua cả hai caller thay vì render() tiếp tục normal flow.
Verification (Xác minh)
Trace tại throw site:
Global
└── render(null)
└── getDisplayName(null)
└── normalizeUser(null) ← throwUnwind nếu không có catch:
normalizeUser() exits abruptly
↓
getDisplayName() exits abruptly
↓
render() exits abruptly
↓
unhandled outside this chainRoot Cause (Nguyên nhân gốc rễ)
Một assumption ở normalizeUser() được biểu diễn bằng exception, nhưng call chain không có boundary phù hợp để chuyển failure này thành behavior mà render() có thể xử lý.
Fix (Sửa lỗi)
Ở phạm vi lab này, đặt boundary tại render():
function render(user) {
console.log("render:start");
try {
const name = getDisplayName(user);
console.log("name:", name);
} catch (error) {
console.log("Cannot render user");
}
console.log("render complete");
}Bây giờ exception unwind normalizeUser() và getDisplayName(), nhưng dừng tại render().
Prevention (Phòng ngừa)
- Xác định rõ function nào có quyền recover và function nào chỉ nên propagate failure.
- Test cả success path lẫn failure path.
- Khi code "bỗng dừng giữa function", trace throw origin và caller chain trước khi thêm
try...catchngẫu nhiên. - Không catch chỉ để làm lỗi biến mất; boundary phải có behavior rõ ràng sau khi catch.
Debugging Lens (Góc nhìn gỡ lỗi)
Khi thấy một statement không chạy dù không có return trước nó, hãy kiểm tra xem một callee có thể throw hay không. Exception propagation là một dạng control transfer có thể bỏ qua nhiều statement và nhiều caller frame.
11. Design / Decision Exercise (Bài tập lựa chọn triển khai)
Một utility readSettings() có thể throw khi file cấu hình không hợp lệ. Team cân nhắc hai boundary:
function readSettings() {
try {
return parseSettings();
} catch (error) {
return {};
}
}function readSettings() {
return parseSettings();
}
function startApp() {
try {
const settings = readSettings();
boot(settings);
} catch (error) {
showStartupError();
}
}Câu hỏi: Phiên bản nào tốt hơn?
Đáp án tham khảo
Không thể quyết định chỉ bằng syntax.
Ở depth L3–L4, hãy dùng contract lens:
- Nếu
{}là fallback hợp lệ vàreadSettings()thực sự sở hữu quyết định fallback, boundary thấp có thể phù hợp. - Nếu parsing failure làm application không thể boot an toàn, biến lỗi thành
{}có thể che mất nguyên nhân và tạo state sai. Boundary cao cho caller có đủ context để quyết định failure behavior. - Điểm cần chứng minh là bạn biết catch location thay đổi exception propagation và observable contract.
Bài này chưa yêu cầu thiết kế error architecture cho toàn hệ thống.
Decision Lens (Góc nhìn quyết định)
Đặt catch ở nơi có đủ context để quyết định recover, fallback, transform hay propagate. Đừng chọn boundary chỉ vì đó là nơi dễ thêm try...catch nhất.
12. Production Scenario (Tình huống thực tế)
Một checkout flow gọi nhiều lớp utility:
function parsePrice(raw) {
const value = Number(raw);
if (!Number.isFinite(value)) {
throw new Error("Invalid price");
}
return value;
}
function calculateTotal(item) {
return parsePrice(item.price) * item.quantity;
}
function submitOrder(item) {
const total = calculateTotal(item);
console.log("submit", total);
}Production nhận một item có price: "unknown". parsePrice() throw, làm calculateTotal() và submitOrder() không hoàn tất theo normal path.
Context: checkout action là user-facing boundary.
Constraint: invalid data phải tạo feedback rõ ràng, không được silently submit NaN và cũng không nên làm cả action chết mà không có message.
Symptom: log submit không xuất hiện; stack trace trỏ từ parsePrice qua calculateTotal tới submitOrder.
Câu hỏi:
- Throw origin nằm ở đâu?
- Frame nào bị unwound nếu không có
catch? - Boundary nào có đủ context để biến technical failure thành user-facing behavior?
- Vì sao catch ngay trong
parsePrice()rồi trả0có thể làm thay đổi business behavior?
Đáp án tham khảo
- Throw origin là
parsePrice()khiNumber.isFinite(value)sai. - Nếu không có handler,
parsePrice()→calculateTotal()→submitOrder()đều kết thúc đột ngột theo propagation path. - Một boundary quanh checkout action hoặc
submitOrder()thường có nhiều context hơn để quyết định message/recovery so với parser thấp nhất. Lesson này chỉ cần nhận ra boundary ownership, chưa thiết kế UX architecture chi tiết. - Trả
0biến "invalid price" thành một total có vẻ hợp lệ. Đây không chỉ là xử lý exception; nó thay đổi data/business contract và có thể che lỗi upstream.
Production Risk (Rủi ro production)
Catch quá thấp rồi trả một fallback "cho chạy tiếp" có thể biến failure rõ ràng thành dữ liệu sai âm thầm. Stack unwinding giúp bạn thấy nơi exception đi qua; engineering judgment quyết định boundary nào thực sự có quyền recover.
13. AI-Assisted Exercise (Bài tập với AI)
Level 2 — Challenge (Thách thức)
- Tự trace trước đoạn
a() → b() → c(), trong đóc()throw vàa()catch. - Ghi Call Stack tại throw site và Call Stack khi
catchcủaa()bắt đầu chạy. - Hỏi AI: "Explain JavaScript stack unwinding when c() throws, b() has no catch, and a() catches the error. Which frames are removed, and does a() get popped before its catch runs?"
- Kiểm tra xem AI có nói sai rằng mọi frame, kể cả frame chứa catch, đều bị pop rồi mới chạy catch hay không.
- Verify bằng một runnable example và quan sát output/stack trace trong DevTools hoặc Node.js.
Đáp án tham khảo
Tại throw site:
Global
└── a()
└── b()
└── c() ← throwNếu c() và b() không có handler, hai invocation đó bị unwound. Khi catch trong a() bắt đầu chạy:
Global
└── a() ← catch đang chạyMột câu trả lời AI nói "a cũng bị popped, sau đó catch của a chạy" là mental model sai. Handler chạy trong execution của a(); frame này vẫn active tại boundary.
14. Teach Back (Dạy lại)
Trong 2 phút, giải thích cho một junior vì sao đoạn code sau không in b:end, nhưng vẫn in a:end:
function a() {
try {
b();
} catch (error) {
console.log("caught");
}
console.log("a:end");
}
function b() {
c();
console.log("b:end");
}
function c() {
throw new Error("boom");
}
a();Bắt buộc dùng đủ các từ: throw site, propagation, stack unwinding, catch boundary, resume.
Mô phỏng
c() là throw site. Vì c() không có catch, exception propagate ra caller và invocation c() kết thúc đột ngột. b() cũng không có catch, nên b() bị stack unwinding trước khi kịp chạy console.log("b:end"). Exception tiếp tục tới a(), nhưng lời gọi b() nằm trong try có catch boundary. Vì vậy frame a() không bị pop; control chuyển vào catch và in caught. Sau khi handler hoàn tất mà không throw thêm, a() resume theo flow sau try...catch, nên console.log("a:end") vẫn chạy.
Gợi ý đánh giá bản thân
- Bạn có nói rõ frame nào bị pop và frame nào vẫn active không?
- Bạn có tránh câu mơ hồ "error đi lên" mà không giải thích mechanism không?
- Bạn có giải thích được vì sao statement sau call site trong
trybị bỏ qua nhưng statement sau toàn bộtry...catchvẫn có thể chạy không?
15. Assessment (Đánh giá)
| Objective (Mục tiêu) | Hình thức đánh giá | Depth (Độ sâu) |
|---|---|---|
| Trace synchronous exception qua 3–4 calls | Trace (Theo vết thực thi) | L3 |
| Giải thích stack unwinding bằng frame lifecycle | Explain (Giải thích) | L2–L3 |
Dự đoán statement bị skip sau throw | Prediction (Dự đoán) | L3 |
| Xác định catch boundary và stack còn lại | Classification + Trace | L3 |
| Phân biệt normal return với exception propagation | Compare (So sánh) | L3 |
| Debug swallowed/rethrown exception | Debug (Gỡ lỗi) | L4 |
| Chọn catch boundary theo ownership đơn giản | Implementation choice | L3–L4 |
| Dùng stack trace để reconstruct caller chain | Debug evidence | L4 |
16. Exit Criteria (Tiêu chí qua bài)
- [ ] Có thể vẽ Call Stack tại throw site của một nested call 3–4 tầng.
- [ ] Có thể ghi đúng thứ tự frame bị unwound khi không có handler ở các tầng trung gian.
- [ ] Có thể xác định chính xác frame chứa
catchboundary vẫn active khi handler bắt đầu chạy. - [ ] Có thể dự đoán đúng ít nhất 4/5 scenario về statement nào chạy hoặc bị skip sau exception.
- [ ] Có thể phân biệt normal
returnvới exception propagation bằng control-flow reasoning. - [ ] Có thể debug một case exception bị swallow và giải thích observable contract đã thay đổi thế nào.
- [ ] Có thể giải thích stack trace như evidence của throw origin + caller chain mà không phụ thuộc format cụ thể của một runtime.
- [ ] Có thể chọn một catch boundary đơn giản dựa trên nơi có đủ context để recover/fallback/propagate.
- [ ] Không nhầm synchronous exception unwinding với Promise rejection hoặc async callback behavior.
17. Spiral Connection (Liên kết xoắn ốc)
Previous (Trước): Lesson 1.5.3 — Recursion đã cho bạn thấy nhiều invocation active cùng lúc và cách Call Stack shrink bằng normal completion theo LIFO. Exception tái sử dụng cùng stack structure nhưng thay đổi lý do frame rời stack: invocation có thể kết thúc đột ngột thay vì return bình thường.
Current (Hiện tại): Exception & Stack Unwinding bổ sung một control-flow path mới vào execution model:
throw → propagation → unwind → catch boundary / unhandled. Bạn giờ có thể giải thích vì sao nhiều caller bị bỏ qua statement còn lại và frame nào vẫn tồn tại khi handler chạy.
Next (Tiếp theo): Lesson 1.5.5 — Full Execution Trace sẽ kết hợp Call Stack, value flow, scope, lexical environment, closure và exception thành một bài trace tổng hợp. Exception handling sau đó được spiral lại ở Stage 3 với Promise rejection/async flow, Stage 5 với network failure, Stage 8 với React error handling và các Stage production/reliability ở depth cao hơn.