Câu 1try { console.log(1); } finally { console.log(2); } sẽ hiển thị gì?
Bắt ngoại lệ — try / catch / finally
Bắt ngoại lệ lúc chạy bằng try và catch, dọn dẹp trong finally, ném lại bằng throw và rẽ nhánh theo loại lỗi.
Khi bạn dùng JSON.parse để đưa một chuỗi nhận được trở lại thành object, chỉ cần chuỗi sai một ký tự là chương trình dừng ngay tại dòng đó, và phần hiển thị hay lưu dữ liệu viết bên dưới đều không chạy. Bạn cũng không thể dùng if để chặn trước mọi nguyên nhân khiến nó dừng.
Bài này nói về try / catch để bắt lấy ngoại lệ đã làm code dừng lại, và finally là khối luôn được chạy.
Bắt lấy ngoại lệ — try và catch
Giả sử bạn muốn parse chuỗi cài đặt nhận từ server rồi áp dụng lên trang. Nếu chuỗi bị sai định dạng, JSON.parse sẽ ném ra một ngoại lệ (exception — lỗi làm gián đoạn quá trình xử lý) ngay tại dòng đó, và không dòng nào bên dưới được chạy. Trang vẫn trống trơn, và mọi thứ dừng ở đó.
Khi ngoại lệ xảy ra bên trong try (khối bao quanh đoạn code có thể thất bại), chương trình chuyển sang catch (khối nhận ngoại lệ). error trong catch (error) chỉ là tên bạn đặt cho giá trị nhận được (đổi thành err hay tên khác cũng chạy bình thường). Nó chứa name là tên loại lỗi, và message là phần mô tả lỗi.
// Chuỗi còn sót dấu phẩy thừa ở cuối
const configText = '{"theme":"dark","fontSize":14,}';
try {
const config = JSON.parse(configText);
console.log(config.theme); // dòng này không được chạy
} catch (error) {
// khi có ngoại lệ, chương trình nhảy vào đây
console.log("Không tải được cài đặt"); // Không tải được cài đặt
console.log(error.name); // SyntaxError
}
// nhờ có catch nên các dòng tiếp theo vẫn chạy
console.log("Hiển thị bằng cài đặt mặc định"); // Hiển thị bằng cài đặt mặc định
Một khi đã chuyển sang catch, chương trình không quay lại chạy tiếp dòng đã ném ngoại lệ, nên hãy viết phần hiển thị thay thế hoặc giá trị mặc định bên trong catch. Nội dung của message khác nhau tùy trình duyệt, nên khi cần phân biệt loại lỗi, hãy đọc name.
Chạy dù thành công hay thất bại — finally
Những việc dọn dẹp như ẩn chỉ báo đang tải hay đóng kết nối đã mở thì cần làm dù xử lý thành công hay thất bại. Nếu viết cùng một dòng ở cả try lẫn catch, sau này bạn dễ sửa chỗ này mà quên chỗ kia, và hai nhánh sẽ chạy lệch nhau.
finally (khối luôn được chạy khi rời khỏi try) được viết sau catch. Dù try chạy hết đến cuối hay catch đã nhận ngoại lệ, nó luôn được chạy ngay trước khi rời đi. Bạn cũng có thể bỏ catch và chỉ viết try với finally.
function loadReport(text) {
try {
const report = JSON.parse(text);
console.log(report.title);
} catch (error) {
console.log("Tải thất bại");
} finally {
// luôn chạy khi rời khỏi try
console.log("Đã ẩn chỉ báo đang tải");
}
}
// Khi thành công
loadReport('{"title":"Doanh thu tháng 9"}'); // Doanh thu tháng 9 → Đã ẩn chỉ báo đang tải
// Khi thất bại
loadReport('{"title":}'); // Tải thất bại → Đã ẩn chỉ báo đang tải
finally chạy sau khi phần code trong try hoặc catch đã xong. Vì vậy trong đoạn code phía trên, thông báo dọn dẹp xuất hiện sau tiêu đề khi thành công, và sau thông báo lỗi khi thất bại.
Không đọc được biến khai báo trong try từ bên ngoài
Biến khai báo bằng const hoặc let bên trong try chỉ dùng được trong chính khối đó. Nếu đọc nó từ catch, finally hay các dòng phía sau thì sẽ gặp ReferenceError. Với những giá trị cần dùng về sau, hãy khai báo bằng let trước try, rồi chỉ gán giá trị bên trong try.
Báo cho phía gọi — throw bên trong catch
Giả sử một hàm đọc dữ liệu đặt chỗ bắt ngoại lệ bằng catch rồi chỉ ghi log. Khi đó hàm vẫn kết thúc bình thường, phía gọi không hề biết đã có lỗi, tiếp tục dùng giá trị trả về, rồi lại dừng ở một dòng khác cách xa nguyên nhân thật.
Nếu truyền nguyên error mà catch nhận được cho throw (câu lệnh ném ra ngoại lệ), cùng ngoại lệ đó sẽ được chuyển tiếp lên phía gọi. Hàm vẫn tự ghi log được, còn việc xử lý thế nào thì để phía gọi quyết định.
function readEntry(text) {
try {
return JSON.parse(text);
} catch (error) {
console.log(`Không đọc được dữ liệu đặt chỗ: ${error.name}`);
throw error; // ném lại nguyên ngoại lệ đã nhận
}
}
try {
const entry = readEntry('{"name":"Linh","seats":}');
console.log(entry.name); // dòng này không được chạy
} catch (error) {
console.log("Đã hủy đặt chỗ"); // Đã hủy đặt chỗ
}
Nếu phía gọi cũng cần xử lý ngoại lệ đã bị bắt bên trong hàm, hãy ném lại nó bằng cách viết throw error; ở cuối catch. Các dòng sau throw không được chạy, nên dòng ghi log phải đặt trước nó.
catch rỗng khiến lỗi như chưa từng xảy ra
Để trống catch (error) { } thì ngoại lệ trông như đã biến mất, nhưng việc thất bại không được ghi lại ở đâu cả, nên sau này bạn không lần ra được nguyên nhân. Kết quả hay gặp nhất là một trang cứ trống trơn. Hãy luôn làm ít nhất một trong các việc sau: hiển thị thông báo, dùng giá trị mặc định, hoặc ném lại.
Rẽ nhánh theo loại lỗi — instanceof và name
catch nhận mọi ngoại lệ, bất kể loại gì. Nhưng thông báo bạn muốn hiển thị cho người dùng sẽ khác nhau tùy theo dữ liệu bị sai định dạng hay giá trị cần đọc không tồn tại.
Với instanceof (toán tử kiểm tra giá trị bên trái có được tạo từ loại bên phải hay không), bạn kiểm tra được loại lỗi, chẳng hạn error instanceof SyntaxError. error.name cũng chứa tên của loại đó dưới dạng chuỗi.
const applicant = { name: "Minh", contact: null };
try {
console.log(applicant.contact.email);
} catch (error) {
if (error instanceof SyntaxError) {
console.log("Dữ liệu sai định dạng"); // không đi vào đây
} else if (error instanceof TypeError) {
console.log("Không có giá trị cần đọc"); // Không có giá trị cần đọc
} else {
console.log(`Loại lỗi ngoài dự kiến: ${error.name}`);
}
console.log(error.name); // TypeError
}
catch (error), nhưng mỗi cái có name khác nhau. Nhờ kiểm tra loại lỗi, bạn hiển thị được thông báo riêng cho từng loại.Những ngoại lệ không khớp với điều kiện nào sẽ được gom vào nhánh else cuối cùng. Bảng dưới đây liệt kê 4 loại lỗi bạn hay gặp và trường hợp mỗi loại xảy ra.
| Loại | Xảy ra khi | Ví dụ code gây ra lỗi |
|---|---|---|
| SyntaxError | Chuỗi không đúng định dạng JSON | JSON.parse("{,}") |
| TypeError | Đọc thuộc tính của null hoặc undefined | applicant.contact.email |
| RangeError | Truyền giá trị vượt quá phạm vi cho phép, chẳng hạn số chữ số | (1980).toFixed(200) (số chữ số tối đa là 100) |
| ReferenceError | Đọc một tên chưa từng được khai báo | console.log(total) |
Kiểm tra kiến thức
Hãy trả lời từng câu hỏi một.
Câu 2Nếu catch trong một hàm chỉ ghi log mà không có throw error;, phía gọi sẽ nhận được giá trị trả về nào?
Câu 3Toán tử nào dùng để kiểm tra loại của error mà catch nhận được?