Cho dù chúng ta có giỏi lập trình đến đâu, đôi khi các tập lệnh của chúng ta cũng có lỗi. Chúng có thể xảy ra do lỗi của chúng ta, đầu vào của người dùng không mong muốn, phản hồi của máy chủ bị lỗi và vì hàng ngàn lý do khác.
Thông thường, một đoạn script Chết (ngay lập tức dừng lại) trong trường hợp có lỗi, in nó ra console.
Nhưng có một cấu trúc cú pháp try..catch
cho phép chúng ta bắt lỗi các lỗi, vì vậy tập lệnh có thể, thay vì chết, làm điều gì đó hợp lý hơn.
Nội dung chính
1. Cú pháp của “try…catch”
Cấu trúctry..catch
có hai khối chính: try
và sau đó catch
:
try {
// code...
} catch (err) {
// error handling
}
Nó hoạt động như thế này:
- Đầu tiên, code trong
try {...}
được thực thi. - Nếu không có lỗi, thì vào
catch(err)
, nếu khôn lỗi thì được bỏ qua: việc thực thi đến hếttry
và tiếp tục, bỏ quacatch
. - Nếu xảy ra lỗi, thì việc
try
thực thi bị dừng lại và dòng điều khiển chảy đến đầucatch(err)
. Biếnerr
(chúng ta có thể sử dụng bất kỳ tên cho nó) sẽ chứa một đối tượng lỗi với các chi tiết về những gì đã xảy ra.
Vì vậy, một lỗi bên trong khốitry {…}
không giết chết tập lệnh – chúng ta có cơ hội xử lý nó trong catch
.
Hãy xem xét một số ví dụ.
- Một ví dụ không có lỗi: hiển thị
alert
(1)
và(2)
:
/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/
try {
alert('Start of try runs'); // (1) <--
// ...no errors here
alert('End of try runs'); // (2) <--
} catch(err) {
alert('Catch is ignored, because there are no errors'); // (3)
}
Một ví dụ có lỗi: hiển thị (1)
và (3)
:
try {
alert('Start of try runs'); // (1) <--
lalala; // error, variable is not defined!
alert('End of try (never reached)'); // (2)
} catch(err) {
alert(`Error has occurred!`); // (3) <--
}
try..catch
chỉ hoạt động cho các lỗi trong thời gian chạy
Để try..catch
làm việc, code phải được chạy. Nói cách khác, nó phải là JavaScript hợp lệ.
Nó sẽ không hoạt động nếu code sai về mặt cú pháp, ví dụ, nó có dấu ngoặc nhọn không khớp:
try {
{{{{{{{{{{{{
} catch(e) {
alert("The engine can't understand this code, it's invalid");
}
Công cụ JavaScript trước tiên đọc code và sau đó chạy nó. Các lỗi xảy ra trong giai đoạn đọc được gọi là lỗi thời gian phân tích và không thể phục hồi (từ bên trong code đó). Đó là bởi vì động cơ không thể hiểu được code.
Vì vậy, try..catch
chỉ có thể xử lý các lỗi xảy ra trong mã hợp lệ. Các lỗi như vậy được gọi là lỗi thời gian chạy và được ..catch
bắt.
Nếu có một ngoại lệ xảy ra trong code trong quá trình chạy, như trong setTimeout
, thì try..catch
sẽ không bắt được:
try {
setTimeout(function() {
noSuchVariable; // script will die here
}, 1000);
} catch (e) {
alert( "won't work" );
}
Đó là bởi vì hàm chính được thực thi sau đó, khi động cơ đã rời khỏi cấu trúctry..catch
.
Để bắt một ngoại lệ bên trong một hàm được lên lịch, try..catch
phải nằm trong hàm đó:
setTimeout(function() {
try {
noSuchVariable; // try..catch handles the error!
} catch {
alert( "error is caught here!" );
}
}, 1000);
2. Đối tượng lỗi
Khi xảy ra lỗi, JavaScript sẽ tạo một đối tượng chứa các chi tiết về nó. Đối tượng sau đó được chuyển qua làm đối số cho catch
:
try {
// ...
} catch(err) { // <-- the "error object", could use another word instead of err
// ...
}
Đối với tất cả các lỗi tích hợp, đối tượng lỗi có hai thuộc tính chính:
name
Tên lỗi. Ví dụ, đối với một biến không xác định đó là "ReferenceError"
. message
Tin nhắn văn bản về chi tiết lỗi.
Có các thuộc tính không phải chuẩn khác có sẵn trong hầu hết các môi trường. Một trong những sử dụng và hỗ trợ rộng rãi nhất là:
stack
Ngăn xếp lệnh gọi hiện tại: một chuỗi với thông tin về chuỗi các cuộc gọi lồng nhau dẫn đến lỗi. Được sử dụng cho mục đích gỡ lỗi.
Ví dụ:
try {
lalala; // error, variable is not defined!
} catch(err) {
alert(err.name); // ReferenceError
alert(err.message); // lalala is not defined
alert(err.stack); // ReferenceError: lalala is not defined at (...call stack)
// Can also show an error as a whole
// The error is converted to string as "name: message"
alert(err); // ReferenceError: lalala is not defined
}
3. Tùy chọn “catch”
Nếu chúng ta không cần chi tiết lỗi, catch
có thể bỏ qua:
try {
// ...
} catch { // <-- without (err)
// ...
}
4. Dùng “try…catch”
Hãy khám phá một trường hợp sử dụng thực tế của try..catch
.
Như chúng ta đã biết, JavaScript hỗ trợ phương thức JSON.parse(str) để đọc các giá trị được code hóa JSON.
Thông thường, nó được sử dụng để giải mã dữ liệu nhận được qua mạng, từ máy chủ hoặc nguồn khác.
Chúng tôi nhận được nó và gọi JSON.parse
như thế này:
let json = '{"name":"John", "age": 30}'; // data from the server
let user = JSON.parse(json); // convert the text representation to JS object
// now user is an object with properties from the string
alert( user.name ); // John
alert( user.age ); // 30
Bạn có thể tìm thấy thông tin chi tiết hơn về JSON trong các phương thức JSON, chương toJSON.
Nếu json
không đúng định dạng, JSON.parse
sẽ tạo ra lỗi, do đó, tập lệnh chết chết.
Chúng ta có nên hài lòng với điều đó? Dĩ nhiên là không!
Bằng cách này, nếu có gì đó không đúng với dữ liệu, khách truy cập sẽ không bao giờ biết điều đó (trừ khi họ mở bảng điều khiển dành cho developer). Và mọi người thực sự không thích khi một thứ gì đó chỉ chết mà không có thông báo lỗi.
Hãy sử dụng try..catch
để xử lý lỗi:
let json = "{ bad json }";
try {
let user = JSON.parse(json); // <-- when an error occurs...
alert( user.name ); // doesn't work
} catch (e) {
// ...the execution jumps here
alert( "Our apologies, the data has errors, we'll try to request it one more time." );
alert( e.name );
alert( e.message );
}
Ở đây chúng ta chỉ sử dụng khốicatch
để hiển thị thông báo, nhưng chúng ta có thể làm nhiều hơn thế: gửi yêu cầu mạng mới, đề xuất một giải pháp thay thế cho khách truy cập, gửi thông tin về lỗi đến cơ sở ghi nhật ký, lỗi. Tất cả tốt hơn nhiều so với chỉ chết.
5. Ném lỗi của chúng ta
Điều gì xảy ra nếu json
đúng về mặt cú pháp, nhưng không có thuộc tínhname
bắt buộc ?
Như thế này:
let json = '{ "age": 30 }'; // incomplete data
try {
let user = JSON.parse(json); // <-- no errors
alert( user.name ); // no name!
} catch (e) {
alert( "doesn't execute" );
}
Ở đây JSON.parse
chạy bình thường, nhưng sự vắng mặt name
thực sự là một lỗi cho chúng ta.
Để thống nhất xử lý lỗi, chúng ta sẽ sử dụng toán tửthrow
.
6. Toán tử Throw
Các toán tửthrow
tạo ra một lỗi.
Cú pháp là:
throw <error object>
Về mặt kỹ thuật, chúng ta có thể sử dụng bất cứ thứ gì như một đối tượng lỗi. Đó có thể là ngay cả một kiểu nguyên thủy, giống như một số hoặc một chuỗi, nhưng nó tốt hơn để đối tượng sử dụng, tốt hơn với name
và thuộc tínhmessage
.
JavaScript có nhiều built-in constructors cho sai số chuẩn: Error
, SyntaxError
, ReferenceError
, TypeError
và những cái khác. Chúng ta có thể sử dụng chúng để tạo các đối tượng lỗi là tốt.
Cú pháp của họ là:
let error = new Error(message);
// or
let error = new SyntaxError(message);
let error = new ReferenceError(message);
// ...
Đối với các lỗi tích hợp (không phải cho bất kỳ đối tượng nào, chỉ cho các lỗi), thuộc tínhname
chính xác là tên của hàm tạo. Và message
được lấy từ các đối số.
Ví dụ:
let error = new Error("Things happen o_O");
alert(error.name); // Error
alert(error.message); // Things happen o_O
Hãy xem loại lỗi nào JSON.parse
tạo ra:
try {
JSON.parse("{ bad json o_O }");
} catch(e) {
alert(e.name); // SyntaxError
alert(e.message); // Unexpected token b in JSON at position 2
}
Như chúng ta có thể thấy, đó là một SyntaxError
.
Và trong trường hợp của chúng ta, sự vắng mặt name
là một lỗi, vì người dùng phải có một name
.
Vì vậy, hãy ném ra nó:
/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Group: https://www.facebook.com/groups/cafedev.vn/
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
Pinterest: https://www.pinterest.com/cafedevvn/
YouTube: https://www.youtube.com/channel/UCE7zpY_SlHGEgo67pHxqIoA/
*/
let json = '{ "age": 30 }'; // incomplete data
try {
let user = JSON.parse(json); // <-- no errors
if (!user.name) {
throw new SyntaxError("Incomplete data: no name"); // (*)
}
alert( user.name );
} catch(e) {
alert( "JSON Error: " + e.message ); // JSON Error: Incomplete data: no name
}
Trong dòng (*)
, toán tửthrow
tạo một SyntaxError
cho message
, giống như cách JavaScript sẽ tự tạo. Việc thực hiện try
ngay lập tức dừng lại và dòng điều khiển nhảy vào catch
.
Bây giờ catch
đã trở thành một nơi duy nhất cho tất cả các xử lý lỗi: cả cho JSON.parse
và các trường hợp khác.
7. Rethrowing
Trong ví dụ trên, chúng ta sử dụng try..catch
để xử lý dữ liệu không chính xác. Nhưng có thể có một lỗi không mong muốn khác xảy ra trong khối try {...}
không? Giống như một lỗi lập trình (biến không được xác định) hoặc một cái gì đó khác, không chỉ điều này dữ liệu không chính xác.
Ví dụ:
let json = '{ "age": 30 }'; // incomplete data
try {
user = JSON.parse(json); // <-- forgot to put "let" before user
// ...
} catch(err) {
alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined
// (no JSON Error actually)
}
Tất nhiên, mọi thứ đều có thể! Lập trình viên làm sai. Ngay cả trong các tiện ích nguồn mở được sử dụng bởi hàng triệu người trong nhiều thập kỷ – đột nhiên một lỗi có thể được phát hiện dẫn đến các vụ hack khủng khiếp.
Trong trường hợp của chúng tôi, try..catch
có nghĩa là bắt lỗi dữ liệu không chính xác về lỗi dữ liệu Nhưng theo bản chất của nó, catch
nhận được tất cả các lỗi từ try
. Ở đây, nó nhận được một lỗi không mong muốn, nhưng vẫn hiển thị cùng một thông báo"JSON Error"
. Điều đó sai và cũng làm cho code khó gỡ lỗi hơn.
May mắn thay, chúng ta có thể tìm ra lỗi nào chúng ta nhận được, ví dụ từ lỗi của nó name
:
try {
user = { /*...*/ };
} catch(e) {
alert(e.name); // "ReferenceError" for accessing an undefined variable
}
Quy tắc rất đơn giản:
Catch chỉ nên xử lý các lỗi mà nó biết và tải lại tất cả các lỗi khác.
Kỹ thuật có thể được giải thích chi tiết hơn như:
- Bắt được tất cả các lỗi.
- Trong khối
catch(err) {...}
chúng tôi phân tích các đối tượng lỗierr
. - Nếu chúng ta không biết cách xử lý nó, chúng ta sẽ làm
throw err
.
Trong code bên dưới, chúng ta sử dụng tính năng kiểm tra lại để catch
chỉ xử lý SyntaxError
:
/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/
let json = '{ "age": 30 }'; // incomplete data
try {
let user = JSON.parse(json);
if (!user.name) {
throw new SyntaxError("Incomplete data: no name");
}
blabla(); // unexpected error
alert( user.name );
} catch(e) {
if (e.name == "SyntaxError") {
alert( "JSON Error: " + e.message );
} else {
throw e; // rethrow (*)
}
}
Lỗi ném (*)
từ bên trong khốicatch
rơi ra khỏi try..catch
và có thể bị bắt bởi một cấu trúctry..catch
bên ngoài (nếu nó tồn tại) hoặc nó giết chết tập lệnh.
Vì vậy, khốicatch
thực sự chỉ xử lý các lỗi mà nó biết cách xử lý và bỏ qua tất cả các lỗi khác.
Ví dụ dưới đây cho thấy các lỗi như vậy có thể bị bắt bởi một cấp độ nữa try..catch
:
function readData() {
let json = '{ "age": 30 }';
try {
// ...
blabla(); // error!
} catch (e) {
// ...
if (e.name != 'SyntaxError') {
throw e; // rethrow (don't know how to deal with it)
}
}
}
try {
readData();
} catch (e) {
alert( "External catch got: " + e ); // caught it!
}
Ở đây readData
chỉ biết cách xử lý SyntaxError
, còn bên ngoài try..catch
biết cách xử lý mọi việc.
8. try…catch…finally
Đợi đã, đó không phải là tất cả.
Cấu rúc try..catch
có thể có thêm một mệnh đề : finally
.
Nếu nó tồn tại, nó chạy trong mọi trường hợp:
- sau đó
try
, nếu không có lỗi - sau
catch
, nếu có lỗi
Cú pháp mở rộng trông như thế này:
try {
... try to execute the code ...
} catch(e) {
... handle errors ...
} finally {
... execute always ...
}
Hãy thử chạy code này:
try {
alert( 'try' );
if (confirm('Make an error?')) BAD_CODE();
} catch (e) {
alert( 'catch' );
} finally {
alert( 'finally' );
}
Code này có hai cách thực hiện:
- Nếu bạn trả lời “Có” khi hỏi “Make an error?”, Sau đó
try -> catch -> finally
. - Nếu bạn nói là No, thì
try -> finally
.
Các điều khoảnfinally
thường được sử dụng khi chúng ta bắt đầu làm một cái gì đó và muốn hoàn thành nó trong bất kỳ trường hợp kết quả.
Chẳng hạn, chúng ta muốn đo thời gian mà hàm số Fibonacci fib(n)
. Đương nhiên, chúng ta có thể bắt đầu đo trước khi nó chạy và kết thúc sau đó. Nhưng nếu có lỗi trong khi gọi hàm thì sao? Cụ thể, việc thực hiện fib(n)
trong code dưới đây trả về lỗi cho các số âm hoặc không nguyên.
Điều khoảnfinally
này là một nơi tuyệt vời để hoàn thành các phép đo bất kể điều gì.
Ở đây finally
đảm bảo rằng thời gian sẽ được đo chính xác trong cả hai tình huống – trong trường hợp thực hiện thành công fib
và trong trường hợp có lỗi trong đó:
/*
Cafedev.vn - Kênh thông tin IT hàng đầu Việt Nam
@author cafedevn
Contact: cafedevn@gmail.com
Fanpage: https://www.facebook.com/cafedevn
Instagram: https://instagram.com/cafedevn
Twitter: https://twitter.com/CafedeVn
Linkedin: https://www.linkedin.com/in/cafe-dev-407054199/
*/
let num = +prompt("Enter a positive integer number?", 35)
let diff, result;
function fib(n) {
if (n < 0 || Math.trunc(n) != n) {
throw new Error("Must not be negative, and also an integer.");
}
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}
let start = Date.now();
try {
result = fib(num);
} catch (e) {
result = 0;
} finally {
diff = Date.now() - start;
}
alert(result || "error occurred");
alert( `execution took ${diff}ms` );
Bạn có thể kiểm tra bằng cách chạy code bằng cách nhập 35
vào prompt
– nó sẽ thực thi bình thường, try
sau đófinally
. Và sau đó nhập -1
– sẽ có một lỗi ngay lập tức và việc thực thi sẽ diễn ra 0ms
. Cả hai phép đo đều được thực hiện chính xác.
Nói cách khác, hàm có thể kết thúc bằng return
hoặc throw
, điều đó không quan trọng. Điều khoản thực thifinally
trong cả hai trường hợp.
Các biến là cục bộ bên trong try..catch..finally
Xin lưu ý rằng result
và diff
các biến trong code ở trên được khai báo trước try..catch
.
Mặt khác, nếu chúng ta khai báo let
trong khốitry
, nó sẽ chỉ hiển thị bên trong nó.finally
và return
finally
làm việc cho bất kỳ lối ra từ try..catch
. Điều đó bao gồm một rõ ràng một return
.
Trong ví dụ dưới đây, có một return
trong try
. Trong trường hợp này, finally
được thực thi ngay trước khi điều khiển trở về code bên ngoài.
function func() {
try {
return 1;
} catch (e) {
/* ... */
} finally {
alert( 'finally' );
}
}
alert( func() ); // first works alert from finally, and then this one
try..finally
Cấu trúctry..finally
, không có mệnh đềcatch
, cũng hữu ích. Chúng ta áp dụng nó khi chúng ta không muốn xử lý lỗi ở đây (để chúng xảy ra), nhưng muốn chắc chắn rằng các quy trình mà chúng ta đã bắt đầu được hoàn tất.
function func() {
// start doing something that needs completion (like measurements)
try {
// ...
} finally {
// complete that thing even if all dies
}
}
Trong đoạn code trên, một lỗi bên trong try
luôn rơi ra, bởi vì không có catch
. Nhưng finally
hoạt động trước khi dòng thực thi rời khỏi hàm.
9. Catch toàn cầu
Đặc thù môi trường
Thông tin từ phần này không phải là một phần của JavaScript cốt lõi.
Hãy tưởng tượng chúng ta đã có một lỗi nghiêm trọng bên ngoài try..catch
và kịch bản đã chết. Giống như một lỗi lập trình hoặc một số điều khủng khiếp khác.
Có cách nào để phản ứng về những sự cố như vậy không? Chúng ta có thể muốn ghi lại lỗi, hiển thị một cái gì đó cho người dùng (thông thường họ không thấy thông báo lỗi), v.v.
Không có gì trong đặc tả, nhưng môi trường thường cung cấp nó, bởi vì nó thực sự hữu ích. Chẳng hạn, Node.js process.on("uncaughtException")
dành cho điều đó. Và trong trình duyệt, chúng ta có thể gán một hàm cho thuộc tính window.onerror đặc biệt, nó sẽ chạy trong trường hợp có lỗi.
Cú pháp:
window.onerror = function(message, url, line, col, error) {
// ...
};
message
Thông báo lỗi.
url
URL của tập lệnh nơi xảy ra lỗi.
line
, col
Số dòng và cột nơi xảy ra lỗi.
error
Đối tượng lỗi.
Ví dụ:
<script>
window.onerror = function(message, url, line, col, error) {
alert(`${message}\n At ${line}:${col} of ${url}`);
};
function readData() {
badFunc(); // Whoops, something went wrong!
}
readData();
</script>
Vai trò của trình xử lý toàn cầu window.onerror
thường không phải là phục hồi thực thi tập lệnh – Nó không thể làm gì với code trong trường hợp có lỗi lập trình, nhưng gửi thông báo lỗi cho developer.
Ngoài ra còn có các dịch vụ web cung cấp ghi nhật ký lỗi cho các trường hợp như vậy, như https://errorception.com or http://www.muscula.com.
Họ làm việc như thế này:
- Chúng ta đăng ký tại dịch vụ và nhận một đoạn code (hoặc URL tập lệnh) từ chúng để chèn vào các trang.
- Tập lệnh JS đó thiết lập một
window.onerror
chức năng tùy chỉnh . - Khi xảy ra lỗi, nó sẽ gửi một yêu cầu mạng về nó tới dịch vụ.
- Chúng ta có thể đăng nhập vào giao diện web dịch vụ và thấy lỗi.
10. Tóm lược
Cấu trúctry..catch
cho phép xử lý lỗi thời gian chạy. Theo nghĩa đen, nó cho phép dùng thử các ứng dụng và chạy các lỗi có thể xảy ra trong đó.
Cú pháp là:
try {
// run this code
} catch(err) {
// if an error happened, then jump here
// err is the error object
} finally {
// do in any case after try/catch
}
Có thể không có phầncatch
hoặc không finally
, vì vậy các cấu trúc ngắn hơn try..catch
và try..finally
cũng hợp lệ.
Các đối tượng lỗi có các thuộc tính sau:
message
– thông báo lỗi có thể đọc được của con người.name
– chuỗi có tên lỗi (tên hàm xây dựng lỗi).stack
(không chuẩn, nhưng được hỗ trợ tốt) – ngăn xếp tại thời điểm tạo lỗi.
Nếu không cần một đối tượng lỗi, chúng ta có thể bỏ qua nó bằng cách sử dụng catch {
thay vì catch(err) {
.
Chúng ta cũng có thể tạo ra lỗi của chúng ta bằng cách sử dụng toán tử throw
. Về mặt kỹ thuật, đối số throw
có thể là bất cứ điều gì, nhưng thông thường, đó là một đối tượng lỗi kế thừa từ lớpError
tích hợp. Xem Thêm về mở rộng lỗi trong chương tiếp theo.
Rethrowing là một mô hình xử lý lỗi rất quan trọng: một khối catch
thường mong đợi và biết cách xử lý loại lỗi cụ thể, vì vậy nó nên điều chỉnh lại các lỗi mà nó không biết.
Ngay cả khi chúng ta không có try..catch
, hầu hết các môi trường đều cho phép chúng ta thiết lập trình xử lý lỗi toàn cầu để bắt lỗi khi mà rơi ra. Trong trình duyệt, đó là window.onerror
.
Full series tự học Javascript từ cơ bản tới nâng cao tại đây nha.
Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của cafedev để nhận được nhiều hơn nữa:
Chào thân ái và quyết thắng!