Error Handling With Promise Trong JavaScript

Chào các bạn!

Ở bài học trước chúng ta đã cùng nhau tìm hiểu về Promises Chaining và chắc chắn các bạn có thể thấy được Promise chains vô cùng hiệu quả trong việc xử lí lỗi. Bài học hôm nay chúng ta cũng sẽ tìm hiểu về các cách xử lí lỗi nữa nhưng bằng các cách khác nhau. Nào bắt đầu thôi!

Như các bạn đã biết Promise chain rất tốt trong việc xử lý lỗi. Khi một promise bị từ chối, trình điều khiển sẽ chuyển nó đến trình xử lý từ chối gần nhất trong chuỗi. Điều đó rất thuận tiện trong thực tế. Ví dụ: Trong đoạn code bên dưới, URL là sai (Vì không có địa chỉ trang web nào như vậy) và .catch sẽ xử lý lỗi đó:

fetch('https://no-such-server.blabla') // rejects
  .then(response => response.json())
  .catch(err => alert(err)) // TypeError: failed to fetch (the text may vary)

Như bạn có thể thấy, .catch có thể xuất hiện sau một hoặc có thể một vài .then hoặc có thể mọi thứ đều ổn với trang web, nhưng phản hồi không phải là JSON hợp lệ. Vì vậy cách dễ nhất để bắt tất cả các lỗi đó là thêm .catch vào cuối chuỗi như sau:

fetch('/article/promise-chaining/user.json')
  .then(response => response.json())
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  .then(response => response.json())
  .then(githubUser => new Promise((resolve, reject) => {
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser);
    }, 3000);
  }))
  .catch(error => alert(error.message));

Nếu bất kỳ promises nào ở trên bị từ chối (do sự cố mạng hoặc json không hợp lệ hoặc bất cứ điều gì), thì .catch sẽ bắt được các lỗi đó.

Implicit try...catch

Các đoạn code của chúng ta có thể vô hình có một try..catch xung quanh để khi có sự cố nào đó xảy ra lỗi đó sẽ bị từ chối. Ví dụ chúng ta có 2 đoạn code:

new Promise((resolve, reject) => {
  throw new Error("Whoops!");
}).catch(alert); // Error: Whoops!

new Promise((resolve, reject) => {
  reject(new Error("Whoops!"));
}).catch(alert); // Error: Whoops!

Hai đoạn code trên hoạt động giống nhau. Tuy nhiên  try..catch vô hình xung quanh sẽ tự động bắt lỗi và biến nó thành promises bị từ chối. Điều này xảy ra không chỉ trong hàm thực thi mà còn trong các trình xử lý của nó. Nếu chúng ta throw bên trong một .then, điều đó có nghĩa là một promises bị từ chối, do đó trình điều khiển sẽ chuyển nó đến trình xử lý lỗi gần nhất. Đây là một ví dụ:

new Promise((resolve, reject) => {
  resolve("ok");
}).then((result) => {
  throw new Error("Whoops!"); // rejects the promise
}).catch(alert); // Error: Whoops!

Điều này xảy ra đối với tất cả các lỗi, không chỉ những lỗi do câu lệnh throw gây ra . Ví dụ:

new Promise((resolve, reject) => {
  resolve("ok");
}).then((result) => {
  blabla(); // no such function
}).catch(alert); // ReferenceError: blabla is not defined

.catch lúc này không chỉ bắt những lời từ chối rõ ràng, mà còn cả những lỗi vô tình do người xử lý ở trên.

Rethrowing

Như chúng ta thấy ở trên, .catch ở cuối chuỗi tương tự như try..catch. Chúng ta có thể có nhiều  .then tùy thích và sau đó sử dụng một trình xử lý duy nhất .catch ở cuối để xử lý lỗi trong tất cả chúng. Thông thường, với try..catch chúng ta có thể phân tích lỗi và có thể rethrow nó nếu không thể xử lý được. Điều tương tự cũng có thể xảy ra đối với promises.

Nếu chúng ta throw bên trong .catch, thì trình điều khiển sẽ chuyển nó đến trình xử lý lỗi gần nhất tiếp theo. Và nếu chúng ta xử lý lỗi và kết thúc bình thường, thì nó sẽ tiếp tục đến .then gần nhất. Trong ví dụ bên dưới, .catch xử lý lỗi thành công:

// the execution: catch -> then
new Promise((resolve, reject) => {

  throw new Error("Whoops!");

}).catch(function(error) {

  alert("The error is handled, continue normally");

}).then(() => alert("Next successful handler runs"));

Ở đây .catch được kết thúc bình thường. Vì vậy, trình xử lý .then thành công tiếp theo được gọi. Trong ví dụ tiếp theo, chúng ta thấy tình huống khác với .catch. Trình xử lý (*)bắt lỗi và không thể xử lý nó (ví dụ: nó chỉ biết cách xử lý URIError ):

// the execution: catch -> catch
new Promise((resolve, reject) => {

  throw new Error("Whoops!");

}).catch(function(error) { // (*)

  if (error instanceof URIError) {
    // handle it
  } else {
    alert("Can't handle such error");

    throw error; // throwing this or another error jumps to the next catch
  }

}).then(function() {
  /* doesn't run here */
}).catch(error => { // (**)

  alert(`The unknown error has occurred: ${error}`);
  // don't return anything => execution goes the normal way

});

Quá trình thực hiện sẽ nhảy từ bước đầu tiên .catch (*) sang bước tiếp theo trong chuỗi (**).

Unhandled rejections

Vậy điều gì xảy ra khi một lỗi không được xử lý? Ví dụ: chúng ta đã quên thêm .catch vào cuối chuỗi như sau:

new Promise(function() {
  noSuchFunction(); // Error here (no such function)
})
  .then(() => {
    // successful promise handlers, one or more
  }); // without .catch at the end!

Trong trường hợp có lỗi, promises sẽ bị từ chối và việc thực thi sẽ chuyển đến trình xử lý từ chối gần nhất. Nhưng không có, vì vậy, lỗi bị "kẹt". Điều gì xảy ra khi một lỗi thường xuyên xảy ra và không bị bắt bởi try..catchScript sẽ chết với một thông báo ở console. Một điều tương tự cũng xảy ra với việc không xử lí các promises bị từ chối. Vì vậy mà công cụ JavaScript giúp ta theo dõi những từ chối như vậy. Bạn có thể thấy nó ở console nếu bạn chạy ví dụ trên.

Trong trình duyệt, chúng ta cũng có thể gặp các lỗi như vậy bằng cách sử dụng  unhandledrejection:

window.addEventListener('unhandledrejection', function(event) {
  // the event object has two special properties:
  alert(event.promise); // [object Promise] - the promise that generated the error
  alert(event.reason); // Error: Whoops! - the unhandled error object
});

new Promise(function() {
  throw new Error("Whoops!");
}); // no catch to handle the error

Nếu có lỗi xảy ra và không có .catchunhandledrejection sẽ kích hoạt và lấy event của đối tượng và thông tin về lỗi, vì vậy chúng ta có thể khắc phục nó thông qua các thông báo về lỗi. Tuy nhiên thông thường những lỗi như vậy không thể khôi phục được, vì vậy cách tốt nhất của chúng ta là thông báo cho người dùng về sự cố và có thể báo cáo sự cố cho máy chủ.

Tổng kết

Vậy là bài học thứ 4 trong series tìm hiểu về promises, async/await của chúng ta đã khép lại. Hẹn gặp lại các bạn trong bài học tiếp theo về Promises API. Còn bây giờ chúng ta sẽ điểm lại những nét chính trong bài học ngày hôm nay:

  • .catch có thể xử lý lỗi trong hầu hết các promises: có thể là một reject() hay một lỗi được trong trình xử lý.
  • Chúng ta nên đặt .catch chính xác ở những nơi mà chúng ta muốn xử lý lỗi và biết cách xử lý. Trình xử lý nên phân tích các lỗi và nên loại bỏ các đến từ quá trình lập trình.
  • Trong mọi trường hợp, chúng ta nên có unhandledrejection trình xử lý sự kiện (đối với trình duyệt và tương tự đối với các môi trường khác) để theo dõi các lỗi chưa được khắc phục và thông báo cho người dùng (và có thể là máy chủ) về chúng, để ứng dụng của chúng ta không bao giờ “chết”.
Trần Quang Hào
SUNTECH VIỆT NAM   Đăng ký để nhận thông báo mới nhất