Debug có hệ thống: tìm bug nhanh, không đoán mò

Debug lâu không phải vì bug khó, mà vì cách tiếp cận sai: đoán mò, sửa lung tung, đổi nhiều thứ cùng lúc. Bài này đưa cho bạn một quy trình debug có hệ thống, biến việc “săn bug” thành các bước kiểm chứng logic. Kết quả: bạn tìm nguyên nhân gốc nhanh hơn và không tạo thêm bug mới trong lúc sửa.

Bản chất của việc debug

Debug là quá trình thu hẹp không gian giả thuyết. Ban đầu bug có thể nằm ở bất kỳ đâu. Mỗi bước debug tốt là một thí nghiệm giúp bạn loại bỏ một nửa khả năng, giống như tìm kiếm nhị phân. Đoán mò thì ngược lại: bạn thử một chỗ ngẫu nhiên, đúng thì may, sai thì vẫn không biết gì hơn.

Một câu nói nổi tiếng của Brian Kernighan đáng nhớ: debug khó gấp đôi việc viết code. Nên nếu bạn viết code “khôn” hết mức có thể, theo định nghĩa bạn không đủ khôn để debug nó. Bài học: giữ code đơn giản, và giữ quy trình debug có kỷ luật.

Quy trình debug từng bước

Bước 1: Tái hiện ổn định

Nếu bạn chưa tái hiện được bug một cách chắc chắn, mọi thứ sau đó chỉ là đoán. Tìm ra bộ input tối thiểu và các bước chính xác làm bug xuất hiện. Bug “thỉnh thoảng lỗi” thường liên quan tới thời gian, thứ tự, dữ liệu cụ thể hoặc trạng thái đồng thời — hãy ghi lại điều kiện khi nó xảy ra.

Bước 2: Đọc thông báo lỗi cho kỹ

Stack trace nói cho bạn dòng nào, hàm nào, kiểu lỗi gì. Đọc từ trên xuống để tìm dòng đầu tiên thuộc code của bạn. Rất nhiều dev bỏ qua bước này rồi mất một giờ cho thứ mà thông báo lỗi đã chỉ thẳng.

Bước 3: Đặt giả thuyết, rồi kiểm chứng

Nói thành lời: “Tôi nghĩ biến này bị null tại đây.” Sau đó kiểm chứng bằng log hoặc breakpoint. Nếu đúng, thu hẹp tiếp. Nếu sai, bạn vẫn học được một điều chắc chắn. Điểm mấu chốt: mỗi lần chỉ thay đổi một thứ.

Bước 4: Thu hẹp phạm vi

Chia đôi đường đi của dữ liệu. Đặt log ở giữa: nếu dữ liệu đã sai tại đó thì bug ở nửa trước, nếu còn đúng thì bug ở nửa sau. Lặp lại vài lần là khoanh vùng được. Với lịch sử Git, git bisect làm đúng ý tưởng này để tìm commit gây lỗi.

Ví dụ thực tế

Một API trả về tổng tiền sai lệch vài đồng ở một số đơn hàng. Đoán mò sẽ đổ cho “lỗi làm tròn” và sửa đại. Cách có hệ thống: tái hiện với đúng đơn hàng lỗi, log giá trị từng bước tính toán. Phát hiện dữ liệu vào đã đúng, tổng trung gian đúng, nhưng bước nhân thuế dùng kiểu số thực (float) thay vì kiểu thập phân chính xác. Nguyên nhân gốc là kiểu dữ liệu, không phải “làm tròn chung chung”. Sửa đúng chỗ, các đơn khác không bị ảnh hưởng.

Lỗi tư duy thường gặp và cách sửa

Đổi nhiều thứ cùng lúc

Sửa ba chỗ rồi bug biến mất, bạn không biết chỗ nào thực sự có tác dụng, và có thể vừa che giấu một bug khác. Cách sửa: mỗi lần một thay đổi, kiểm chứng, rồi mới tiếp.

Tin vào giả định thay vì dữ liệu

“Hàm này chắc chắn chạy đúng” là câu nói khiến bạn mù. Cách sửa: đừng tin, hãy in ra. Log giá trị thực tế thay vì tưởng tượng nó là gì.

Bỏ qua bước tái hiện

Sửa một bug không tái hiện được thì bạn không bao giờ biết mình đã sửa xong chưa. Cách sửa: đầu tư thời gian để tái hiện trước, kể cả khi khó.

Không dùng công cụ có sẵn

Nhiều người chỉ dùng print mà bỏ qua debugger. Debugger cho bạn dừng, xem toàn bộ trạng thái, đi từng dòng. Cách sửa: học phím tắt debugger của ngôn ngữ bạn dùng.

Checklist khi gặp bug

  • Đã tái hiện được bug một cách ổn định chưa?
  • Đã đọc kỹ toàn bộ thông báo lỗi và stack trace chưa?
  • Giả thuyết hiện tại của mình là gì, và mình kiểm chứng nó bằng cách nào?
  • Mình có đang chỉ thay đổi một thứ mỗi lần không?
  • Dữ liệu sai bắt đầu xuất hiện từ điểm nào trong luồng?
  • Bug mới xuất hiện gần đây? Thử git bisect để tìm commit gốc
  • Sau khi sửa, đã viết một test để bug này không quay lại chưa?

Kết luận và bước tiếp theo

Debug là kỹ năng rèn được, không phải năng khiếu bẩm sinh. Lần tới khi gặp bug, hãy ép mình viết ra giả thuyết trước khi sờ vào code. Bước tiếp theo: chọn một bug bạn từng “sửa may rủi” và thử áp dụng quy trình bốn bước ở trên để hiểu thật sự nguyên nhân gốc.

Câu hỏi thường gặp

Khi nào nên dùng debugger, khi nào nên dùng log?

Debugger tốt khi bạn cần khám phá trạng thái phức tạp tại một thời điểm. Log tốt khi bạn cần theo dõi một luồng qua thời gian, hoặc debug trên môi trường production nơi không gắn debugger được. Hai công cụ bổ sung cho nhau.

Làm sao debug bug chỉ xảy ra trên production?

Tăng cường log có ngữ cảnh (request id, user id, input), thu thập chúng tập trung, và cố gắng tái hiện trong môi trường giống production nhất có thể. Bug production thường liên quan đến dữ liệu thật, tải cao hoặc cấu hình khác biệt.

Bug đồng thời (race condition) debug thế nào?

Loại này khó vì không ổn định. Hãy thu hẹp vùng dùng chung tài nguyên, thêm log kèm timestamp và thread id, và xem lại các đoạn không được đồng bộ hóa. Đôi khi cố ý thêm độ trễ giúp bug lộ ra ổn định hơn để quan sát.

Nên bỏ bao lâu tự debug trước khi nhờ người khác?

Không có con số cứng, nhưng một mốc thực dụng: khi bạn kẹt và đã hết giả thuyết mới trong khoảng nửa giờ đến một giờ, hãy giải thích vấn đề cho đồng nghiệp. Chỉ việc trình bày lại mạch lạc đã thường giúp bạn tự nhận ra chỗ sai.

Nguồn tham khảo

  • “Debugging” (David J. Agans) — sách về chín quy tắc tìm lỗi
  • “The Practice of Programming” (Kernighan & Pike) — nguyên tắc lập trình và gỡ lỗi
  • Tài liệu về git bisect trong sách Pro Git

Similar Posts