Debug cho người mới: cách tìm và sửa lỗi có hệ thống

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Có một sự thật mà ít sách giáo khoa nói thẳng: lập trình viên dành phần lớn thời gian để tìm và sửa lỗi, không phải để viết code mới. Debug là kỹ năng, và như mọi kỹ năng, nó có phương pháp để học.

Các loại lỗi

  • Lỗi cú pháp: viết sai ngữ pháp (thiếu ngoặc, thiếu dấu hai chấm). Chương trình không chạy được.
  • Lỗi khi chạy: chương trình chạy rồi mới sập, ví dụ chia cho 0 hoặc truy cập phần tử không tồn tại.
  • Lỗi logic: chương trình chạy bình thường nhưng cho kết quả sai. Đây là loại khó nhất vì không có thông báo lỗi nào.

Quy trình 5 bước

  1. Tái hiện lỗi: tìm đúng bước khiến lỗi xuất hiện, và cố gắng làm nó xảy ra ổn định. Không tái hiện được thì khó sửa.
  2. Thu hẹp phạm vi: xác định đoạn code nào có thể gây ra lỗi.
  3. Đặt giả thuyết: "Tôi nghĩ biến X đang bị rỗng ở bước này".
  4. Kiểm chứng giả thuyết: in giá trị, đặt breakpoint, viết test.
  5. Sửa và kiểm tra lại: sửa nguyên nhân gốc, rồi chạy lại cả trường hợp lỗi lẫn các trường hợp khác.

Điểm mấu chốt là làm việc theo giả thuyết, không sửa mò. Sửa ngẫu nhiên đến khi hết lỗi thường chỉ che giấu vấn đề.

Kỹ thuật 1: in giá trị để quan sát

Cách đơn giản nhất và vẫn rất hiệu quả: chèn lệnh in để xem chương trình đang làm gì.

def tinh_trung_binh(ds):
    print("ds =", ds)           # kiểm tra đầu vào
    tong = sum(ds)
    print("tong =", tong)
    return tong / len(ds)

print(tinh_trung_binh([]))      # ZeroDivisionError: ds rỗng

Chỉ sau hai dòng in bạn đã thấy danh sách rỗng là thủ phạm. Nhớ xóa các dòng in tạm sau khi sửa xong.

Kỹ thuật 2: dùng debugger và breakpoint

Trình gỡ lỗi (debugger) cho phép tạm dừng chương trình tại một dòng, xem giá trị mọi biến, rồi chạy từng bước:

  • Breakpoint: điểm dừng bạn đặt tại một dòng.
  • Step over: chạy dòng hiện tại, đi tiếp dòng kế.
  • Step into: đi vào bên trong hàm được gọi.
  • Watch: theo dõi một biểu thức qua từng bước.

VS Code, PyCharm, Visual Studio, Chrome DevTools đều có debugger tích hợp. Học dùng nó sớm sẽ tiết kiệm nhiều giờ.

Kỹ thuật 3: chia để trị (bisect)

Nếu không biết lỗi nằm ở đâu trong đoạn code dài, hãy chia đôi: kiểm tra kết quả ở giữa. Nếu giá trị giữa đã sai, lỗi nằm ở nửa đầu, ngược lại ở nửa sau. Lặp lại để thu hẹp nhanh. Trong Git, lệnh git bisect áp dụng cùng ý tưởng để tìm commit gây lỗi.

Kỹ thuật 4: rubber duck debugging

Giải thích code từng dòng cho một con vịt cao su (hoặc một người bạn, hoặc ghi ra giấy). Khi buộc phải nói rõ, bạn thường tự nhận ra chỗ sai trước khi nói hết câu. Cách này rất hiệu quả khi bạn bí.

Kỹ thuật 5: thu nhỏ ví dụ

Tách đoạn gây lỗi thành chương trình nhỏ nhất vẫn còn lỗi. Quá trình rút gọn này thường tự bộc lộ nguyên nhân, và nếu cần hỏi người khác, một ví dụ nhỏ dễ được giúp đỡ hơn nhiều.

Những thói quen giúp ít lỗi hơn

  • Viết và chạy thử từng đoạn nhỏ.
  • Đặt tên biến rõ nghĩa để dễ nhận ra sai sót.
  • Kiểm tra đầu vào và xử lý trường hợp biên (rỗng, âm, 0).
  • Dùng kiểm soát phiên bản để quay lại khi hỏng.
  • Nghỉ giải lao: đầu óc mệt rất dễ bỏ sót lỗi hiển nhiên.

Ví dụ gỡ lỗi từng bước

Xét hàm sau, được cho là trả về số lớn thứ hai nhưng đôi khi sai:

def lon_thu_hai(ds):
    lon, nhi = ds[0], ds[0]
    for x in ds:
        if x > lon:
            nhi = lon
            lon = x
        elif x > nhi:
            nhi = x
    return nhi

print(lon_thu_hai([5, 9, 7]))   # 7, đúng
print(lon_thu_hai([9, 5, 7]))   # 9, sai! mong đợi 7

Áp dụng quy trình: tái hiện bằng đầu vào [9, 5, 7]. Thu hẹp: ngay từ đầu lon và nhi đều bằng 9, và 5, 7 đều không lớn hơn nhi = 9. Giả thuyết: khởi tạo nhi sai. Kiểm chứng bằng cách in lon, nhi sau mỗi lượt. Sửa: khởi tạo nhi bằng âm vô cực, và bỏ qua giá trị bằng lon:

def lon_thu_hai(ds):
    lon, nhi = float("-inf"), float("-inf")
    for x in ds:
        if x > lon:
            nhi, lon = lon, x
        elif lon > x > nhi:
            nhi = x
    return nhi

print(lon_thu_hai([9, 5, 7]))   # 7

Sau khi sửa, hãy chạy thêm các trường hợp biên: mảng có hai số bằng nhau, mảng một phần tử. Một bản sửa tốt luôn đi kèm vài ca kiểm thử để lỗi cũ không quay lại.

Kết luận

Debug là điều tra: tái hiện, thu hẹp, đặt giả thuyết, kiểm chứng, sửa. Đừng thấy lỗi là nản; mỗi lỗi bạn tự tìm ra là một bài học nhớ lâu. Hãy bắt đầu bằng thói quen đơn giản nhất: đọc thông báo lỗi thật kỹ và dùng print có chủ đích.

Học xong lý thuyết, hãy thực hành thật nhiều; các bài giảng có bài tập trong khóa học codebem.com sẽ giúp bạn việc đó.

author_photo
Nguyễn Đình Cường

0 comment