0
0
Lập trình
Hưng Nguyễn Xuân 1
Hưng Nguyễn Xuân 1xuanhungptithcm

Hướng dẫn xử lý lỗi và ghi log với neverthrow trong TypeScript

Đăng vào 1 tháng trước

• 10 phút đọc

Chủ đề:

KungFuTech

Giới thiệu

Bạn có bao giờ gặp phải tình huống trong một dự án TypeScript sử dụng mô hình Result, và nhận ra rằng mặc dù nó hứa hẹn về việc xử lý lỗi an toàn với kiểu dữ liệu, nhưng mã nguồn của bạn lại trở nên lộn xộn với những kiểm tra if (result.isErr()) và việc ghi log không nhất quán? Nếu có, bạn không đơn độc. Sức mạnh của mô hình này có thể bị mất khi mã nguồn của bạn phát triển, đặc biệt nếu bạn chưa hoàn toàn áp dụng phong cách lập trình hàm. Bài viết này sẽ khám phá một giải pháp cho vấn đề phổ biến này, nhằm làm cho việc xử lý lỗi trở nên mạch lạc hơn và khôi phục sự rõ ràng cho mã nguồn của bạn.

Khoảng một năm rưỡi trước, tôi đã phát hiện ra mô hình Result từ lập trình hàm, cho phép tôi thực hiện việc xử lý lỗi an toàn với kiểu dữ liệu trong TypeScript. Có nhiều thư viện cho điều đó, chẳng hạn như ts-results, neverthrow và thư viện tham vọng hơn là Effect, nhưng tất cả đều thực hiện cùng một ý tưởng.

Tôi đã thành công khi sử dụng mô hình này trong các ứng dụng sản xuất của mình, bắt đầu với gói ts-results, sau đó chuyển sang neverthrow (do nó hoàn thiện hơn và được bảo trì tốt hơn), và điều này đã chứng minh là một cách đi đến việc viết các ứng dụng mạnh mẽ hơn, với khả năng quan sát tốt hơn, đạt được một chính sách ghi log nhất quán và xử lý lỗi mạnh mẽ.

Phương pháp ban đầu

Ban đầu, tôi đã sử dụng nó trực tiếp trong tất cả các hàm và phương thức của mình như sau.

typescript Copy
// genericFunction.ts
import { err, ok, Result } from 'neverthrow';

type FunctionError = {
  code: 'UNEXPECTED_ERROR' | 'GENERIC_ERROR_1' | 'GENERIC_ERROR_2' | 'NOT_FOUND';
  data?: Record<string, unknown>;
};

export function genericFunction(input: { param: string }): 
Result<{ param: string }, FunctionError> {
  // ...
  // Có gì đó không ổn
  if (input.param !== 'ok') return err({ code: 'GENERIC_ERROR_1' });

  // Tất cả đều ổn
  return ok({ param: 'ok' });
}

Khi tôi cần gọi một phương thức hoặc hàm bên trong một hàm khác, tôi đã làm như sau:

typescript Copy
// upperFunction.ts
import { err, ok, Result } from 'neverthrow';
import { genericFunction } from './genericFunction';
import { Logger } from '../utils/logger';

const logger = Logger;
type UpperFunctionError = {
  code: 'UNEXPECTED_ERROR' | 'UPPER_ERROR' | 'NOT_FOUND';
  data?: Record<string, unknown>;
};

export function upperFunction(input: { param: string }): 
Result<{ param: string }, UpperFunctionError> {
  const res = genericFunction(input);
  if (res.isErr()) {
    switch (res.error.code) {
      case 'UNEXPECTED_ERROR':
        return err({ code: 'UNEXPECTED_ERROR' });
      case 'GENERIC_ERROR_1': {
        const customError = { code: 'UNEXPECTED_ERROR' as const };
        logger.error('Điều này không nên xảy ra', {
          ...customError,
          params: input
        });
        return err(customError);
      }
      case 'GENERIC_ERROR_2':
        return err({ code: 'UPPER_ERROR' });
      case 'NOT_FOUND':
        return err({ code: 'NOT_FOUND' });
      default:
        return err({ code: 'UNEXPECTED_ERROR' });
    }
  }

  // Tất cả đều ổn, thực hiện bước tiếp theo
  // ...
  return ok({ param: 'ok' });
}

Cách tiếp cận này hoạt động tốt như một bước đầu tiên và chắc chắn có thể được cải thiện theo nhiều cách, chẳng hạn như sử dụng các công cụ lập trình hàm mà thư viện cung cấp, và chắc chắn sẽ có những trường hợp mà một số lỗi là mong đợi và thậm chí là mong muốn và logic của hàm sẽ chuyển đổi chúng thành hành vi thay vì lỗi khác, nhưng điều này nên bao quát ý tưởng chính.

Mặc dù một người có thể nghĩ rằng việc kết hợp các loại lỗi và chỉ đơn giản là truyền bá lỗi là đơn giản hơn, nhưng điều này có thể dẫn đến những lỗi gây nhầm lẫn và vô nghĩa trong các lớp trên của ứng dụng. Ví dụ, việc trả về lỗi GROUP_NOT_FOUND từ một hàm dự kiến lấy các mặt hàng có thể mua cho một nền tảng thương mại điện tử thực sự không cung cấp ngữ cảnh hữu ích cho hàm gọi.

Vấn đề

Nhìn vào mã, chúng ta có thể tưởng tượng mức độ phức tạp của việc xử lý lỗi có thể trở nên như thế nào, đặc biệt trong các dịch vụ phức tạp thực hiện logic kinh doanh, một phương thức có thể trở thành 80% xử lý lỗi lặp đi lặp lại, nơi chúng ta phải xem xét: khi nào để ghi log, ghi log cái gì và cố gắng giữ sự nhất quán trên tất cả các dịch vụ, điều này ngay cả với một trợ lý mã LLM có thể giúp chúng ta trong trình soạn thảo mã, chúng ta có thể mắc lỗi.

Giải pháp

Liệu có phải tốt không nếu chúng ta có một trợ lý thực hiện một tập hợp các quy tắc nhất quán và yêu cầu chúng ta xử lý tất cả các trường hợp lỗi có thể xảy ra, với tính an toàn kiểu và tự động hoàn thành? Tôi đã phát triển một trợ lý cho mục đích này, bạn có thể tìm thấy trong gist GitHub này:

Cách sử dụng

Ý tưởng với trợ lý này là quản lý một chính sách ghi log nhất quán và một phương pháp xử lý lỗi tương tự như những gì được tìm thấy trong ngôn ngữ lập trình Rust, nơi chúng ta phân chia lỗi thành các lỗi có thể làm việc được mong đợi và các lỗi nghiêm trọng mà nên dừng thực thi chương trình và gây ra việc tái triển khai hệ thống.

Để đạt được điều này, trợ lý thực hiện ba chức năng chính:

  1. Nó ép buộc một loại lỗi nhất quán trên tất cả các phương thức. Chúng ta nên sử dụng trợ lý errorBuilder để xây dựng loại lỗi cụ thể mà phải được trả về bởi tất cả các hàm có thể thất bại. Về bản chất, trợ lý này chuẩn hóa loại lỗi cho tất cả các hàm "Result" của chúng ta.

  2. Nó cung cấp một hàm Panic để buộc quá trình dừng lại khi cần thiết. Trường hợp sử dụng chính cho điều này là xác định các tình huống làm cho ứng dụng của bạn không thể hoạt động và nhận được thông tin này ngay lập tức trong môi trường sản xuất. Ví dụ, nếu một biến cấu hình cơ bản hoặc chuỗi kết nối cơ sở dữ liệu bị thiếu trong quá trình xác thực khởi động ứng dụng, chúng ta có thể ngay lập tức gọi Panic. Điều này dừng quá trình, cho phép các nhà phát triển được thông báo thông qua các công cụ quan sát hoặc báo động đám mây rằng có điều gì đó đã xảy ra nghiêm trọng, kích hoạt một lần tái triển khai tự động phiên bản hoạt động cuối cùng của dịch vụ.

Hãy nhớ sử dụng hàm này một cách cẩn thận. Có thể không phải là một ý tưởng hay khi kích hoạt panic cho một lỗi tạm thời như vấn đề kết nối API bên thứ ba hoặc cơ sở dữ liệu. Tùy thuộc vào ngữ cảnh, những điều này có thể là sự kiện bình thường hoặc thậm chí đã lên lịch, chẳng hạn như nhà cung cấp đám mây thực hiện bảo trì và cập nhật cơ sở dữ liệu vào lúc 3:00 sáng.

  1. Sử dụng một số phép thuật TypeScript, nó bao gồm trợ lý errorHandler giúp chúng ta ánh xạ trực tiếp tất cả các mã lỗi có thể xảy ra, loại bỏ mã lặp lại. Nó cũng tự động xử lý ghi log và xây dựng một đối tượng lỗi phù hợp với kiểu trả về mong đợi.

Trình xử lý thực hiện tập hợp các quy tắc sau:

  • Tất cả các reservedErrorCodes sẽ tự động được dịch thành UNEXPECTED_ERROR, được coi là những lỗi không nghiêm trọng không mong đợi, có nghĩa là khi lỗi này được trả về, hãy nói trong catch trong một hàm truy vấn, chúng ta phải ghi log tất cả các chi tiết thất bại, và lỗi sẽ lan truyền như UNEXPECTED_ERROR qua tất cả các cuộc gọi hàm, theo cách đó lỗi chỉ được ghi lại một lần.
  • Khi lỗi được dịch sang cùng một lỗi hoặc một lỗi mong đợi khác, nó chỉ ghi log với mức độ ghi log, như vậy giúp chúng ta theo dõi dấu vết thực thi.
  • Khi lỗi mong đợi được dịch thành UNEXPECTED_ERROR, có nghĩa là khi một lỗi không nên xảy ra được phát hiện, (ví dụ: USER_ALREADY_EXIST khi chúng ta vừa xóa người dùng, do đó có khả năng là một lỗi), lỗi sẽ được ghi với mức độ lỗi, bao gồm tất cả dữ liệu được đưa cho trình xử lý.
  • Khi lỗi được dịch sang PANIC, trình xử lý gọi hàm Panic để ghi lại nguyên nhân của lỗi nghiêm trọng và kết thúc quá trình, điều này nên kích hoạt tái triển khai máy chủ. Tùy thuộc vào dự án và framework, chúng ta có thể cần điều chỉnh hàm này.

Ví dụ tiếp theo bao gồm tất cả các trường hợp đã đề cập:

typescript Copy
import { ok, Result } from 'neverthrow';
import { genericFunction } from './genericFunction';
import { Logger } from '../logger';
import { ErrorBuilder, errorHandlerResult } from 'src/utils/error';

// Xây dựng lỗi hàm sử dụng loại ErrorBuilder

type UpperFunctionError = ErrorBuilder<{ code: 'UPPER_ERROR' | 'NOT_FOUND' }>

export function upperFunction(input: { param: string }): 
Result<{ param: string }, UpperFunctionError> {
  const res = genericFunction(input);

  if (res.isErr()) {
    return errorHandlerResult(res.error, Logger, {
      GENERIC_ERROR_1: {
        code: 'PANIC',
        message: 'Điều này không nên xảy ra',
        extraParams: input
      },
      GENERIC_ERROR_2: {
        code: 'UNEXPECTED_ERROR',
        message: 'Điều này không nên xảy ra nhưng không nghiêm trọng',
        extraParams: input
      },
      NOT_FOUND: { code: 'NOT_FOUND' }
    });
  }

  // Mọi thứ ổn và TypeScript biết
  // ...
  return ok({ param: 'ok' });
}

Áp dụng điều này, chúng ta có thể giảm mã xử lý lỗi trong dịch vụ của mình và tránh việc ghi log thủ công, không chỉ vậy mà chúng ta có thể thay đổi quy tắc ghi log vào bất kỳ thời điểm nào (điều chỉnh trình xử lý đã cung cấp) mà không cần phải thay đổi điều đó trong mỗi dịch vụ.

Trợ lý này bao gồm cả một errorHandler thô và một errorHandlerResult để chúng ta có thể trực tiếp xây dựng đối tượng lỗi kết quả hoặc không, tùy thuộc vào trường hợp, ví dụ: chúng ta có thể sử dụng errorHandler độc lập bên trong một phương thức như phương thức từ Result của neverthrow, tự động chuyển đổi giá trị trả về thành giá trị kết quả.

Các khuyến nghị bổ sung

  • Trình xử lý luôn nhận được phiên bản logger mà nên được sử dụng để ghi log lỗi. Bằng cách này, dễ dàng tích hợp với các giải pháp ghi log tùy chỉnh như pino logger. Nên triển khai một correlationId để theo dõi dấu vết thực thi cho mỗi cuộc gọi dịch vụ.
  • Về các trường hợp ghi log khác, điều đó chắc chắn phụ thuộc vào dự án, nhưng một quy tắc hợp lý là ghi log với mức độ ghi log, tất cả các biến đổi trong một định dạng nhất quán như:
typescript Copy
logger.log('', {
  param1: '123',
  param2: 123
});

Kết luận

Tôi tin rằng việc sử dụng trợ lý này được tùy chỉnh cho nhu cầu dự án của bạn, và tuân theo các khuyến nghị bổ sung, là một điểm khởi đầu tốt sẽ giúp bạn làm cho hệ thống của bạn mạnh mẽ hơn và dễ dàng quan sát hơn.

Nếu bạn thích ý tưởng này và có các chính sách ghi log khác trong tâm trí, hãy cho tôi biết trong các bình luận, có thể tôi có thể biến điều này thành một gói npm nếu có các quy tắc và trường hợp khác được thảo luận, nếu không hãy lấy trợ lý và sử dụng nó trực tiếp trong dự án của bạn.

Hy vọng nó hữu ích.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào