Một hàm gặp lỗi nhưng caller lại nhận chuỗi “đã dọn xong”. Hãy kiểm tra xem khối finally có đang return hay không. Câu lệnh tưởng chỉ phục vụ cleanup có thể thay thế kết quả hoặc exception mà hàm đang chuẩn bị trả ra ngoài.
Các snippet trong bài đã chạy bằng Node.js 24.18.1, đối chiếu MDN ngày 01/11/2026. Ví dụ tạo giá trị và lỗi trong RAM, không mở file hoặc gọi dịch vụ. Mục tiêu là hiểu luồng điều khiển trước khi áp dụng vào logic tài nguyên thật.
1. Return từ try chưa kết thúc toàn bộ construct
Khi đi qua try/finally, return của try phải chờ finally hoàn tất. Nếu finally đưa ra return khác, quyết định mới thay thế quyết định trước đó. Điều này không phải ghép hai kết quả lại với nhau.
import assert from 'node:assert/strict';
function replacedValue() {
try {
return 'kết quả chính';
} finally {
return 'kết quả cleanup';
}
}
assert.equal(replacedValue(), 'kết quả cleanup');
console.log(replacedValue());
Trong fixture, caller nhận kết quả cleanup. Đừng biến mẫu này thành cách “chuẩn hóa” mọi kết quả sau cùng: nó làm người đọc khó thấy giá trị thật được chọn ở nhánh nào.
2. Một throw cũng có thể bị che
Không cần có catch để finally thay đổi luồng. Một lỗi đang chuẩn bị truyền ra ngoài có thể bị return trong finally thay thế.
import assert from 'node:assert/strict';
const original = new Error('lỗi chính');
function maskedError() {
try {
throw original;
} finally {
return 'đã dọn xong';
}
}
assert.equal(maskedError(), 'đã dọn xong');
assert.doesNotThrow(() => maskedError());
Caller không nhận original qua hàm này. Nếu log cleanup khiến bạn tưởng lỗi đã được xử lý đúng, hãy kiểm tra cả giá trị mà caller nhận và nhánh reject/throw, thay vì chỉ kiểm tra một dòng log có được in không.
3. Cleanup không cần return để giữ kết quả
Mẫu đồng bộ dưới đây để task quyết định kết quả, còn cleanup ghi một dấu hiệu local. Hai đường đi thành công và thất bại đều được kiểm tra.
import assert from 'node:assert/strict';
function runWithCleanup(task, cleanup) {
try {
return task();
} finally {
cleanup();
}
}
const trace = [];
assert.equal(runWithCleanup(() => 42, () => trace.push('clean')), 42);
const original = new Error('task failed');
assert.throws(
() => runWithCleanup(() => { throw original; }, () => trace.push('clean')),
error => error === original,
);
assert.deepEqual(trace, ['clean', 'clean']);
Đây là helper cho task đồng bộ trong fixture. Trả một Promise từ task không tự khiến finally đợi Promise đó hoàn tất; đừng dùng helper này rồi tuyên bố tài nguyên async đã được giữ đến cuối công việc.
Nếu chính cleanup ném lỗi, lỗi đó vẫn có thể thay đổi kết quả đi ra. Khi cleanup thật có thể thất bại, cần chính sách giữ hoặc ghi nhận lỗi chính và lỗi cleanup rõ ràng. Việc bỏ return giải quyết một bẫy cụ thể, không đảm bảo mọi thao tác cleanup đều vô hại.
4. Promise.finally có hợp đồng khác
Callback truyền vào Promise.finally là một hàm riêng. Giá trị return thông thường của callback không thay thế giá trị fulfilled ban đầu; nhưng throw hoặc Promise bị reject từ callback có thể làm chuỗi reject.
import assert from 'node:assert/strict';
assert.equal(await Promise.resolve(42).finally(() => 99), 42);
const original = new Error('primary');
await assert.rejects(
Promise.reject(original).finally(() => 'ignored'),
error => error === original,
);
const cleanupError = new Error('cleanup');
await assert.rejects(
Promise.resolve(42).finally(() => { throw cleanupError; }),
error => error === cleanupError,
);
Chạy các snippet có import hoặc top-level await dưới dạng file .mjs. Các assertions đã đạt trên Node nêu trên. Không suy ngữ nghĩa của block finally từ việc hai cấu trúc có cùng tên gọi.
Cách giải thích khi phỏng vấn
Theo dõi ba bước: try hoặc catch định đi ra bằng gì, finally thực hiện gì, rồi caller thực sự nhận gì. Thử riêng return, throw và cleanup cũng throw. Một câu trả lời có fixture kiểm tra lỗi giữ nguyên thường rõ hơn câu “finally luôn chạy” đứng riêng lẻ.
Trong code review, ưu tiên cleanup không thay đổi luồng chính nếu không có chủ đích rõ ràng. Những ví dụ này nói về luồng JavaScript trong runtime đang chạy, không cam kết cleanup khi tiến trình bị kết thúc đột ngột.

