Sử dụng try...catch đúng cách

Nhìn lại một cách cặn kẽ hơn cách chúng ta xử lý trường hợp lỗi trong javascript

Tình huống khi bạn biết chắc nó sẽ có lỗi, nhưng là một lỗi có thể chấp nhận và bỏ qua, bạn sẽ viết nó thế này

const defaultConfig = { foo: 'bar' };
let customConfig = {};
try {
  customConfig = require(path.resolve(process.cwd(), 'custom.config'));
} catch (error) {
  // không có custom config thì cũng ok mà.
}
const config = { ...defaultConfig, ...customConfig };

Nếu có custom.config file, chúng ta load nó, nếu ko thì cũng chẳng sao, chúng ta dùng giá trị mặc định.

Vậy đâu là vấn đề khi chúng ta sử dụng catch mà ko làm gì cả. Vấn đề là chúng ta catch toàn bộ error mặc dù chúng ta không hề biết là có một error nào khác có thể xuất hiện trong try hay ko

// custom.config.js
const foo = 'bar';
foo = 'baz'; // TypeError: Assignment to constant variable.

module.exports = { foo };

Nếu file custom.config của chúng ta mắc lỗi TypeError: Assignment to constant variable như trên, đoạn code load config sẽ vẫn chạy với default config như đã biết, vì nó bỏ qua luôn khi có lỗi trong file custom.config.js.

const defaultConfig = { foo: 'bar' };
 let customConfig = {};
 try {
   customConfig = require(path.resolve(process.cwd(), 'custom.config'));
 } catch (error) {
  // không có custom config thì cũng ok mà.
+  if (error.code !== 'MODULE_NOT_FOUND') throw error;
 }
 const config = { ...defaultConfig, ...customConfig };

Kiểm tra error.code để đảm bảo chỉ bỏ qua các lỗi mà chúng ta thật sự không quan tâm, tình huống này là MODULE_NOT_FOUND, và throw một error cho các trường hợp khác.

Có thể phân error ra làm 2 loại: operational errorprogrammer error. Operational error là các lỗi từ bên ngoài chương trình chúng ta viết, code chúng ta vẫn chạy, nhưng lỗi chúng ta ko kiểm soát được như gọi API bị fail. Programmer error là kiểu lỗi do chúng ta gây ra bên trong source, đọc bài này để hiểu chi tiết hơn

Với kiểu operational error, chúng ta có những cách tiếp cận sau

  • catch lại error và thực hiện lại thao tác đó lần nữa
  • catch error mà ko làm gì cả, hoặc hiển thị một thông báo đến user
  • Ko catch luôn, hoặc throw một custom error

Với lỗi với network request, chúng ta có thể dựa vào error.code trả về để lựa chọn thao tác tiếp theo muốn thực hiện.

// notifications.js
import { fetchNew } from './notification-service';

try {
  const notifications = await fetchNew();
  // ...
} catch (error) {
  if (error.message.match(/Network Error/)) {
    Sentry.withScope((scope) => {
      scope.setLevel(Sentry.Severity.Info);
	    Sentry.captureException(error);
	  });
  } else {
    throw error;
  }
}

Ở ví dụ trên, chúng ta ko thông báo gì cả cho user mà log lại lỗi đó trong Sentry (Sẵn tiện giới thiệu luôn, Sentry là một tool để lưu lại các lỗi nếu có xảy ra trên app, khá hữu ích nhé)

Nếu chúng ta có file article-service.js chứa function thực hiện request API, rải rác ở nhiều nơi khác trong source, sử dụng function này của article-service.js, chúng ta sẽ ko đặt catch error ở trong article-service.js mà đặt ở nơi đang sử dụng

// article-service.js
import api from './api';

export async function list() {
	return api.list({ filter: { type: 'article' } })
}
// article-listing.js
import { list } from './article-service';

// ...

try {
  const articles = await list();
  renderArticles(articles);
} catch(error) {
  Sentry.withScope((scope) => {
    scope.setLevel(Sentry.Severity.Warning);
	  Sentry.captureException(error);
  });
  // hiển thị thông báo
  // để user biết có lỗi chứ
  renderError(error);
}

Tóm lại, một điều quan trọng nhất cần nhớ sau bài này là đừng bao giờ dùng try...catch mà bỏ trống phần catch

try...catch: The Right Way

Sử dụng try...catch đúng cách