Bài 7 · Vận dụng · 20 phút
Xử lý lỗi sạch
Biên soạn bởi Nguyễn Anh Tuấn
Dùng exception thay mã lỗi/null; viết try-catch trước, thông điệp có ngữ cảnh; đừng trả/nhận null (kiểu hẹp, “?”); bọc lỗi bên thứ ba; đừng nuốt lỗi.
Dùng mã lỗi (error code) - return -1, return null, hoặc object {error: string} - bắt caller phải kiểm tra ngay tại chỗ, xen kẽ với logic chính. Kết quả: "đường hạnh phúc" (happy
path) bị chôn giữa đống kiểm tra lỗi. Đây cũng chính là cách xử lý lỗi kiểu C làm - báo lỗi qua giá trị trả về.
❌ Error code - logic chính bị chôn giữa kiểm tra lỗi
function divideWithCode(a: number, b: number): { result?: number; error?: string } {
if (b === 0) return { error: 'DIVIDE_BY_ZERO' };
return { result: a / b };
}
// Caller phai kiem tra truoc khi dung - de quen
const r = divideWithCode(10, 0);
if (r.error) {
console.log('Loi:', r.error);
} else {
console.log('Ket qua:', r.result); // r.result co the undefined - khong an toan
} ✅ Exception - đường hạnh phúc thẳng tắp, lỗi tách riêng
function divide(a: number, b: number): number {
// Fail fast: bao loi ngay khi phat hien trang thai sai
if (b === 0) throw new Error(`Chia cho khong (phep tinh: ${a} / ${b})`);
return a / b;
}
// Duong hanh phuc ro rang; xu ly loi tach ra khoi logic chinh
try {
const result = divide(10, 2);
console.log('Ket qua:', result); // 5
} catch (e: unknown) {
if (e instanceof Error) console.log('Loi:', e.message);
} - ▸Error code: caller bị ép kiểm tra sau mỗi lệnh gọi - dễ quên, trộn lẫn logic chính và xử lý lỗi.
- ▸Exception: đường hạnh phúc thẳng tắp, xử lý lỗi nằm riêng ở catch - dễ đọc hơn nhiều.
- ▸Dùng `catch (e: unknown)` thay vì `any` - TypeScript bắt bạn kiểm tra kiểu trước khi dùng.
Một kỹ thuật hữu ích: viết khung try/catch/finally trước khi bắt đầu một thao tác có thể thất bại. Điều này giúp bạn suy nghĩ về phạm vi của "giao dịch" (transaction) trước khi viết logic bên trong.
Thông điệp lỗi phải kèm ngữ cảnh: tên thao tác, giá trị liên quan, ý định - đủ để debug mà không cần đọc lại code.
Viết khung try/catch trước - thông điệp có ngữ cảnh
// Buoc 1: viet khung truoc de dinh hinh pham vi
async function loadUserProfile(userId: string): Promise<UserProfile> {
// Khung try/catch/finally truoc - roi moi dien logic vao
try {
// logic se viet o day
throw new Error('chua implement');
} catch (e: unknown) {
// Thong diep co: thao tac + gia tri gay loi + y dinh
const msg = e instanceof Error ? e.message : String(e);
throw new Error(`Tai profile that bai [userId=${userId}]: ${msg}`);
}
// finally: giai phong tai nguyen neu co (ket noi DB, file handle...)
}
// Buoc 2: dien logic vao trong try
async function loadUserProfileV2(userId: string): Promise<UserProfile> {
try {
const row = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
if (!row) throw new Error('Khong tim thay ban ghi');
return mapRowToProfile(row);
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
throw new Error(`Tai profile that bai [userId=${userId}]: ${msg}`);
}
} - ▸Viết try/catch trước giúp định hình phạm vi - bạn suy nghĩ về lỗi trước, không phải sau.
- ▸Thông điệp lỗi = tên thao tác + giá trị liên quan + nguyên nhân gốc - đủ để debug.
- ▸finally: luôn chạy dù có lỗi hay không - dùng để giải phóng tài nguyên (kết nối, file).
Null là nguồn gốc của vô số lỗi runtime: caller quên kiểm tra, TypeScript không bắt được nếu không bật strictNullChecks. Thay thế tốt hơn là kiểu hẹp (`T | undefined`) hoặc trả mảng rỗng:
❌ Trả null - caller dễ quên kiểm tra
// Tra null khi khong tim thay - caller co the dung ma khong kiem tra
function findUser(id: string): User | null {
if (id === '42') return { name: 'Meo Con', email: 'meo@example.com' };
return null; // caller co quen null check?
}
const u = findUser('99');
console.log(u.name); // Runtime error: Cannot read properties of null ✅ undefined + strictNullChecks - TypeScript bắt lỗi sớm
// undefined + strictNullChecks: TypeScript ep kiem tra truoc khi dung
function findUser(id: string): User | undefined {
if (id === '42') return { name: 'Meo Con', email: 'meo@example.com' };
return undefined;
}
const u = findUser('99');
if (u === undefined) {
console.log('Khong tim thay nguoi dung'); // Khong tim thay nguoi dung
}
const u2 = findUser('42');
if (u2 !== undefined) {
console.log(u2.name); // Meo Con
}
// Cho danh sach: tra mang rong thay vi null - caller khong can kiem tra null
function getOrders(userId: string): Order[] {
if (userId === 'unknown') return []; // mang rong, khong phai null
return db.findOrders(userId);
}
// caller: getOrders(...).forEach(...) - khong can null check Bật strictNullChecks ngay
- ▸Trả `undefined` thay null cho "không tìm thấy" - và bật strictNullChecks để compiler bắt.
- ▸Trả mảng rỗng `[]` thay null cho danh sách - caller không cần null check trước khi dùng.
- ▸Không truyền null vào hàm - kiểm tra trước khi gọi, hoặc dùng overload rõ ràng.
Khi dùng thư viện ngoài, lỗi của thư viện đó rò ra khắp codebase. Đổi thư viện là phải sửa mọi chỗ catch. Giải pháp: bọc (wrap) lỗi bên thứ ba sau interface của mình.
Bọc lỗi bên thứ ba - caller chỉ biết lỗi của mình
// Lop loi rieng - caller khong import StripeError hay BraintreeError
class PaymentError extends Error {
constructor(
message: string,
public readonly cause?: unknown
) {
super(message);
this.name = 'PaymentError';
}
}
// Wrapper: catch loi ngoai, throw loi cua minh
function chargeCard(amount: number): void {
try {
if (amount <= 0) throw new RangeError('So tien khong hop le');
externalPaymentLib.charge(amount); // thu vien ngoai
} catch (err: unknown) {
// Boc loi: them context, doi kieu exception
throw new PaymentError(
`Thanh toan that bai (so tien: ${amount})`,
err
);
}
}
// Caller chi biet PaymentError - khong phu thuoc thu vien ngoai
try {
chargeCard(50000);
} catch (e: unknown) {
if (e instanceof PaymentError) {
console.log('PaymentError:', e.message); // PaymentError: Thanh toan that bai (so tien: 50000) - neu loi
}
} ❌ Nuốt lỗi (swallowing) - tệ nhất có thể làm
// TUYET DOI KHONG: catch rong - loi bien mat, debug la ac mong
try {
doSomethingRisky();
} catch (e) {
// khong lam gi ca - loi bi "nuot" mat
}
// TUYET DOI KHONG: log roi im lang - doi ngoai khong biet co loi
try {
doSomethingRisky();
} catch (e) {
console.log(e); // log nhung khong xu ly, khong rethrow
// caller van tiep tuc nhu khong co gi xay ra
} - ▸Bọc lỗi bên thứ ba: caller chỉ biết exception của bạn - đổi thư viện không ảnh hưởng caller.
- ▸Đừng bao giờ để catch rỗng: log + rethrow, hoặc recover có chủ đích, hoặc fail fast.
- ▸Fail fast: phát hiện trạng thái sai thì báo ngay - không tiếp tục với dữ liệu hỏng.
Sẽ học thêm ở bài Boundaries
Câu hỏi thường gặp
Trong JavaScript/TypeScript, throw/catch có chi phí cao hơn return bình thường - nhưng chỉ đáng kể nếu exception xảy ra trong vòng lặp nóng hàng triệu lần. Với code ứng dụng thông thường (network, I/O, validation), chi phí này không thể đo được. Ưu tiên rõ ràng và đúng semantic trước; chỉ tối ưu khi đo được bottleneck thực sự.
Dùng catch (e: unknown) - TypeScript 4+ mặc định và đúng. Trong JavaScript mọi thứ đều có thể throw (string, number, object), không chỉ Error. Khai báo any bỏ qua kiểm tra kiểu. Với unknown, bạn phải kiểm tra (e instanceof Error) trước khi dùng - đó là hành vi đúng và bắt được lỗi lập trình sớm.
TypeScript và JavaScript có cả hai, và quy ước không hoàn toàn thống nhất. Một quy ước phổ biến: undefined = "chưa có giá trị / chưa gán" (mặc định của JS), null = "có giá trị, nhưng là không có" (chủ ý gán). Quan trọng hơn quy ước là nhất quán trong dự án, và bật strictNullChecks để compiler bắt cả hai. Tránh trả null từ hàm khi undefined hay mảng rỗng truyền đạt ý định rõ hơn.
Có. Nếu dùng try/catch trực tiếp, caller nhận kiểu exception của thư viện ngoài - khi đổi thư viện thì phải sửa mọi chỗ catch. Bọc lỗi sau interface của mình: caller chỉ biết PaymentError của bạn, không biết đó là StripeError hay BraintreeError. Đổi nhà cung cấp thanh toán không ảnh hưởng caller. Cũng dễ thêm log, retry, hoặc context vào một chỗ.
"Fail fast" nghĩa là: khi phát hiện trạng thái sai, báo ngay thay vì tiếp tục với dữ liệu hỏng rồi crash ở chỗ khác (khó trace). Trong thực tế: validate ở đầu hàm (if (!input) throw new Error(...)), không tiếp tục khi precondition sai. Không có nghĩa là throw bừa - lỗi có thể recover được (retry, default value) thì handle; lỗi bất ngờ không recover được thì throw để caller quyết định.
Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.
Trả lời vài câu để chắc rằng em đã nắm bài.
Exception tốt hơn error code ở điểm nào khi đọc code?
- 1
Đổi mã lỗi thành exception
Tìm một hàm trong code đang trả mã lỗi (return -1, return null khi thất bại, hoặc trả object
{ error: string }). Refactor thành throw Error với thông điệp rõ ngữ cảnh.Hoàn thành khi: Hàm throw Error thay vì trả mã lỗi; caller dùng try/catch; thông điệp lỗi có tên thao tác và giá trị gây lỗi.
- 2
Viết try/catch trước
Chọn một thao tác có thể thất bại (đọc file, gọi API, parse JSON). Viết try/catch/finally trước khi viết logic bên trong - chỉ phần khung. Rồi điền logic vào.
Hoàn thành khi: Khung try/catch/finally rõ ràng; finally giải phóng tài nguyên nếu có; lỗi có thông điệp kèm ngữ cảnh.
- 3
Xoá null khỏi return
Tìm một hàm đang trả
nullkhi không có kết quả. Refactor: dùngundefined, mảng rỗng ([]), hoặc custom type (T | undefined) thay thế. Sửa chỗ gọi.Hoàn thành khi: Hàm không còn trả null; caller xử lý rõ ràng trường hợp không có kết quả bằng kiểm tra
=== undefinedhoặc.length === 0. - 4
Bật strictNullChecks
Nếu dự án của mèo con chưa bật
strictNullCheckstrongtsconfig.json, bật lên. Xem bao nhiêu lỗi xuất hiện. Chọn một lỗi điển hình và sửa.Hoàn thành khi: Ghi lại số lỗi trước và sau; mô tả pattern lỗi phổ biến nhất xuất hiện; sửa ít nhất một lỗi thực sự.
- 5
Bọc lỗi bên thứ ba
Chọn một chỗ đang dùng trực tiếp exception của thư viện ngoài (axios, fetch, prisma...). Tạo class Error riêng của mình (vd
DatabaseError,NetworkError). Bọc lỗi ngoài vào trong hàm wrapper.Hoàn thành khi: Class Error tùy chỉnh kế thừa Error; hàm wrapper catch lỗi ngoài và throw lỗi của mình; caller không còn import kiểu lỗi của thư viện.
- 6
Tìm catch rỗng và sửa
Grep trong codebase tìm
catchrỗng hoặc chỉ có comment. Chọn một chỗ: hoặc log + rethrow, hoặc xử lý lỗi thực sự, hoặc fail fast. Không để catch im lặng.Hoàn thành khi: Chỗ catch không còn nuốt lỗi; có một trong ba: log, recover, hoặc rethrow với context.