Skip to content

0.3 - Debug và đọc thông báo lỗi ​

Tóm tắt: Người mới thường sửa lỗi bằng cách đoán, thêm vài dòng Console.WriteLine, rồi chạy lại cho tới khi hết lỗi. Bài này dạy hai kỹ năng thay thế cách đó. Thứ nhất là đọc stack trace: thông báo lỗi luôn nói lỗi gì, ở file nào, dòng nào. Thứ hai là dùng debugger: dừng chương trình tại một dòng, xem giá trị từng biến, rồi chạy tiếp từng bước. Ví dụ xuyên suốt là một chương trình tính tuổi trung bình của thành viên CLB, có một lỗi làm chương trình sập và một lỗi không sập nhưng tính sai.

Mục tiêu bài học ​

Sau bài này bạn có thể:

  • Đọc một stack trace và chỉ ra loại lỗi, thông điệp, file và dòng gây lỗi.
  • Đặt breakpoint, chạy debug, xem biến ở bảng Variables và Watch.
  • Dùng Step Over, Step Into, Step Out, Continue và breakpoint có điều kiện.
  • Thay int.Parse bằng int.TryParse để xử lý dữ liệu người dùng nhập sai.
  • Nhận ra lỗi "không sập nhưng sai", loại lỗi chỉ debugger hoặc test mới bắt được.

Cần biết trước: đã làm xong 0.2 — Chương trình C# đầu tiên.

Nội dung bài học ​

0.3.1 — Chương trình có lỗi ​

Tạo project mới và thay Program.cs bằng đoạn code sau:

bash
dotnet new console -n DiemDanh
cd DiemDanh
csharp
// Đọc năm sinh thành viên từ form đăng ký CLB (dữ liệu dạng chuỗi, do người dùng gõ)
string[] registeredBirthYears = ["2005", "2004", "2006", "hai nghìn lẻ năm", "2003"];

var totalAge = 0;
for (var i = 0; i < registeredBirthYears.Length; i++)
{
    var birthYear = int.Parse(registeredBirthYears[i]);
    totalAge += 2026 - birthYear;
}

Console.WriteLine($"Tuổi trung bình: {totalAge / registeredBirthYears.Length}");

["2005", ...] là cú pháp collection expression để tạo mảng. Bạn sẽ gặp lại ở bài 1.5.

Chạy dotnet run:

text
Unhandled exception. System.FormatException: The input string 'hai nghìn lẻ năm' was not in a correct format.
   at System.Number.ThrowFormatException[TChar](ReadOnlySpan`1 value)
   at System.Int32.Parse(String s)
   at Program.<Main>$(String[] args) in D:\code\DiemDanh\Program.cs:line 7

0.3.2 — Đọc stack trace từ trên xuống ​

Đọc ba thứ, theo thứ tự:

  1. Loại lỗi: System.FormatException, tức là sai định dạng.
  2. Thông điệp: chuỗi 'hai nghìn lẻ năm' không đúng định dạng số. Thông điệp đã nói gần hết nguyên nhân.
  3. Dòng đầu tiên thuộc code của bạn. Các dòng at ... xếp từ chỗ lỗi xảy ra ra ngoài. Hai dòng đầu nằm trong thư viện .NET (System.Number, System.Int32), bạn không sửa được chúng. Dòng thứ ba là Program.cs:line 7: đó là chỗ cần xem.

Program.<Main>$ là tên hàm Main ẩn mà trình biên dịch tạo ra cho top-level statements (bài 0.2).

Quy tắc chung: bỏ qua các dòng của thư viện, tìm dòng đầu tiên có tên file của bạn. Ở các module sau, stack trace của một Web API có thể dài vài chục dòng, nhưng cách đọc vẫn như vậy.

0.3.3 — Chạy debugger lần đầu ​

Lần này ta dừng chương trình trước khi nó sập để xem chuyện gì xảy ra.

  1. Mở thư mục DiemDanh bằng VS Code, mở Program.cs.
  2. Bấm vào khoảng trống bên trái số dòng 7, hoặc đặt con trỏ ở dòng 7 rồi bấm F9. Một chấm đỏ xuất hiện: đó là breakpoint.
  3. Bấm F5. Nếu VS Code hỏi chọn debugger, chọn C#. C# Dev Kit tự tìm project và chạy.
  4. Chương trình dừng ở dòng 7, dòng đó được tô vàng. Dòng tô vàng chưa chạy, nó là dòng sắp chạy.

Lúc này nhìn sang panel Run and Debug bên trái:

  • Variables hiện mọi biến đang có: i = 0, totalAge = 0, registeredBirthYears (bấm mũi tên để xem từng phần tử).
  • Watch: bấm dấu + và gõ một biểu thức, ví dụ registeredBirthYears[i]. Giá trị được tính lại sau mỗi bước.
  • Di chuột lên một biến trong code cũng thấy giá trị của nó.

Thanh công cụ debug ở trên cùng có các nút sau, phím tắt giống nhau trên VS Code và Visual Studio:

PhímTênLàm gì
F5ContinueChạy tiếp tới breakpoint kế tiếp
F10Step OverChạy dòng hiện tại, dừng ở dòng tiếp theo
F11Step IntoNếu dòng hiện tại gọi một hàm của bạn, đi vào bên trong hàm đó
Shift+F11Step OutChạy nốt hàm hiện tại, quay ra chỗ gọi
Shift+F5StopDừng debug

Rider dùng phím khác ở bộ phím mặc định (F8 Step Over, F7 Step Into). Bạn có thể đổi sang bộ phím Visual Studio trong Settings > Keymap.

Bấm F10 nhiều lần và nhìn i, totalAge thay đổi. Tới vòng i = 3, bấm F10 ở dòng 7 thì chương trình dừng lại báo Exception has occurred: FormatException, kèm đúng thông điệp ở trên.

0.3.4 — Breakpoint có điều kiện ​

Bấm F10 bốn lần để tới vòng lỗi thì còn chịu được. Với danh sách 500 thành viên mà dòng lỗi ở vị trí 437 thì không.

Chuột phải vào chấm đỏ, chọn Edit Breakpoint... (hoặc chuột phải vào lề, chọn Add Conditional Breakpoint...), gõ điều kiện:

csharp
registeredBirthYears[i].Length != 4

Bấm F5 lại. Chương trình chạy thẳng qua các năm sinh có đủ 4 chữ số và chỉ dừng ở dòng dữ liệu đáng ngờ. Điều kiện là một biểu thức C# bình thường, nên viết đơn giản thôi: debugger tính nó ở mỗi lần đi qua breakpoint.

0.3.5 — Sửa lỗi sập: int.TryParse ​

int.Parse ném lỗi khi gặp chuỗi không phải số. Với dữ liệu người dùng gõ, sai định dạng là chuyện bình thường chứ không phải tình huống bất thường. Vì vậy dùng int.TryParse: nó trả về true hoặc false thay vì ném lỗi.

csharp
// Đọc năm sinh thành viên từ form đăng ký CLB (dữ liệu dạng chuỗi, do người dùng gõ)
string[] registeredBirthYears = ["2005", "2004", "2006", "hai nghìn lẻ năm", "2003"];

var totalAge = 0;
var validCount = 0;
foreach (var line in registeredBirthYears)
{
    if (!int.TryParse(line, out var birthYear))
    {
        Console.WriteLine($"Bỏ qua dòng không hợp lệ: \"{line}\"");
        continue;
    }
    totalAge += 2026 - birthYear;
    validCount++;
}

Console.WriteLine($"Tuổi trung bình: {(double)totalAge / validCount:F1} ({validCount} thành viên)");
text
Bỏ qua dòng không hợp lệ: "hai nghìn lẻ năm"
Tuổi trung bình: 21,5 (4 thành viên)

out var birthYear khai báo biến ngay trong lời gọi. Nếu parse thành công, birthYear chứa số đọc được.

Kết quả in 21,5 hay 21.5 tùy cài đặt vùng (culture) của máy: máy đặt định dạng Việt Nam dùng dấu phẩy thập phân. Đây không phải lỗi, nhưng là thứ cần nhớ khi xuất dữ liệu cho máy khác đọc, ví dụ file CSV hay JSON.

0.3.6 — Lỗi nguy hiểm hơn: không sập nhưng sai ​

Đoạn code mới còn sửa thêm hai lỗi mà bản đầu tiên không hề báo:

  1. Chia số nguyên. totalAge / registeredBirthYears.Length là int chia int, nên kết quả bị cắt phần thập phân: 86 / 4 ra 21 chứ không phải 21.5. Bản sửa ép kiểu (double)totalAge trước khi chia.
  2. Chia sai mẫu số. Bản đầu chia cho tổng số dòng (5), kể cả dòng không hợp lệ. Bản sửa chia cho validCount (4).

Hãy chú ý điểm này: nếu dữ liệu không có dòng sai, bản đầu tiên chạy không lỗi và in ra một con số trông hợp lý nhưng sai. Lỗi sập thì ai cũng thấy. Lỗi sai im lặng chỉ lộ ra khi bạn dùng debugger kiểm tra giá trị từng bước, hoặc khi có test so sánh với kết quả đúng. Đó là chủ đề của bài 0.4.

0.3.7 — Lỗi thường gặp ​

Breakpoint là vòng tròn rỗng, chương trình không dừng. Code chưa được lưu, hoặc đang chạy bằng dotnet run trong terminal thay vì F5. Debugger chỉ hoạt động khi bạn chạy bằng F5.

Chương trình có Console.ReadLine bị treo khi debug. Mặc định output khi debug hiện ở cửa sổ Debug Console, và cửa sổ này không nhận dữ liệu nhập vào. Mở Settings (Ctrl+,), tìm csharp.debug.console và chọn integratedTerminal. Từ đó chương trình chạy trong terminal của VS Code, nơi bạn gõ được.

Bấm F11 ở dòng gọi int.Parse nhưng không đi vào bên trong. Đây là hành vi đúng. Mặc định debugger chỉ đi vào code của bạn ("Just My Code"), không vào thư viện .NET.

Thêm Console.WriteLine khắp nơi để debug. Cách này vẫn dùng được cho bài nhỏ, nhưng phải sửa code, chạy lại và nhớ xóa sau đó. Debugger cho bạn xem mọi biến cùng lúc mà không phải sửa dòng code nào.

Bài tập áp dụng ​

Bài 1 — Đọc stack trace không chạy code ​

Chương trình sau sập khi chạy:

csharp
string[] registeredBirthYears = ["2005", "2004", "2006", "2003", "2005"];

var totalAge = 0;
for (var i = 0; i <= registeredBirthYears.Length; i++)
{
    totalAge += 2026 - int.Parse(registeredBirthYears[i]);
}

Console.WriteLine($"Tuổi trung bình: {(double)totalAge / registeredBirthYears.Length:F1}");
text
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at Program.<Main>$(String[] args) in D:\code\DiemDanh\Program.cs:line 6

Chỉ đọc code và stack trace, không chạy: lỗi gì, ở dòng nào, và ký tự nào ở dòng khác là nguyên nhân?

Tiêu chí hoàn thành: bạn chỉ ra đúng ký tự gây lỗi và sửa được mà không cần chạy.

Gợi ý và lời giải — Bài 1

Lỗi: truy cập phần tử nằm ngoài mảng. Stack trace trỏ vào dòng 6, nơi có registeredBirthYears[i]. Mảng 5 phần tử có chỉ số từ 0 tới 4.

Nguyên nhân nằm ở dòng 4, không phải dòng 6. Điều kiện i <= registeredBirthYears.Length cho i chạy tới 5, và registeredBirthYears[5] không tồn tại. Đây là lỗi "lệch một" (off-by-one), một trong những lỗi phổ biến nhất khi viết vòng lặp.

Sửa: đổi <= thành <, hoặc dùng foreach để khỏi phải quản lý chỉ số.

Bài học rút ra: stack trace cho biết nơi lỗi lộ ra, chưa chắc là nơi gây ra lỗi. Từ dòng được báo, hãy lần ngược lên xem giá trị sai được tạo ra ở đâu.

Bài 2 — Săn lỗi im lặng bằng debugger ​

Chương trình sau phải in tổng tiền đơn hàng là 1.170.000, nhưng lại in ra số khác. Đừng sửa ngay: đặt breakpoint, dùng F10 và bảng Variables để tìm vòng lặp nào làm sai.

csharp
decimal[] prices = [250_000m, 400_000m, 520_000m];
int[] quantities = [1, 1, 1];

decimal total = 0;
for (var i = 1; i < prices.Length; i++)
{
    total += prices[i] * quantities[i];
}

Console.WriteLine($"Tổng tiền: {total:N0}");

Tiêu chí hoàn thành: bạn nêu được giá trị của total sau từng vòng lặp, và chỉ ra phần tử nào bị bỏ sót.

Gợi ý và lời giải — Bài 2

Gợi ý. Đặt breakpoint ở dòng total += ..., thêm i và prices[i] vào Watch. Nhìn giá trị i ở lần dừng đầu tiên.

Lời giải. Lần dừng đầu tiên i = 1, không phải 0. Vòng lặp bỏ qua sản phẩm đầu tiên (250.000), nên in ra 920.000 thay vì 1.170.000. Sửa var i = 1 thành var i = 0.

Vì sao debugger nhanh hơn đoán? Nhìn code thì dễ bỏ qua số 1 vì nó trông "bình thường". Debugger cho thấy ngay giá trị thật của i ở vòng đầu, không cần tin vào trí nhớ.

{total:N0} định dạng số có dấu phân cách hàng nghìn, không lấy phần thập phân.

Tự kiểm tra ​

Trong stack trace, nên đọc dòng nào trước?

Đọc loại lỗi và thông điệp ở dòng đầu, rồi tìm dòng at ... đầu tiên có tên file của bạn. Các dòng thuộc thư viện .NET nằm phía trên dòng đó cho biết lỗi xảy ra sâu bên trong hàm nào, nhưng chỗ cần sửa nằm trong code của bạn.

Khi chương trình dừng ở breakpoint, dòng được tô vàng đã chạy chưa?

Chưa. Dòng tô vàng là dòng sắp chạy. Bấm F10 thì dòng đó mới được thực thi.

Khi nào dùng int.Parse, khi nào dùng int.TryParse?

Dùng int.TryParse khi dữ liệu có thể sai một cách bình thường, như người dùng gõ hay file nhập từ ngoài. Dùng int.Parse khi dữ liệu sai nghĩa là có lỗi lập trình, ví dụ một giá trị cấu hình bạn tự viết. Trong trường hợp đó, sập sớm với thông báo rõ ràng là tốt hơn chạy tiếp với dữ liệu hỏng.

Vì sao 86 / 4 trong C# ra 21?

Vì cả hai vế là int, nên phép chia là chia nguyên và phần thập phân bị bỏ. Ép một vế sang double hoặc decimal trước khi chia để có 21.5.

Kết luận ​

  1. Stack trace là bản đồ. Loại lỗi, thông điệp và dòng đầu tiên trong code của bạn thường đã đủ để sửa.
  2. Debugger thay cho việc đoán. Breakpoint cộng với F10 và bảng Variables cho bạn thấy giá trị thật thay vì giá trị bạn tưởng.
  3. Lỗi im lặng nguy hiểm hơn lỗi sập. Chương trình chạy xong không có nghĩa là chạy đúng.

Tham khảo ​

Điều hướng ​

Phát hành theo giấy phép MIT.