1. Làm quen với stack gọi hàm (Call Stack)
Ở bài trước tụi mình có nhắc sơ về stack gọi hàm, giờ cùng đào sâu hơn nhé.
Hình dung nè: user bấm nút trong app. Nút này gọi method OnClick() ("Nút được bấm"). Method này lại gọi LoadData() (tải dữ liệu), rồi nó lại gọi ReadFromFile() (đọc từ file).
Bỗng dưng trong ReadFromFile() có lỗi — file không tìm thấy. Ai là thủ phạm?
Để tìm hiểu, chương trình sẽ "lần ngược dấu vết": từ ReadFromFile() → LoadData() → OnClick(). Đường đi này gọi là stack gọi hàm — giống như chồng đĩa, cái trên cùng rớt trước.
Chương trình sẽ đi xuống chồng này cho tới khi tìm được catch phù hợp, nhưng vẫn chạy hết tất cả finally trên đường đi — để mọi thứ được đóng, giải phóng và dọn dẹp.
Trong lập trình, stack gọi hàm là một danh sách nhớ ai gọi ai, để khi có gì đó, ta có thể lần ngược "đường đi" này tìm nguyên nhân lỗi.
Cách nó hoạt động
Khi chương trình chạy, mỗi lần gọi method (hoặc function) sẽ thêm một "dòng" mới vào stack (danh sách) gọi hàm. Nếu trong quá trình chạy có exception, class .NET Exception sẽ lưu thông tin về stack này: ở đâu và theo thứ tự nào các method đã được gọi dẫn đến lỗi.
2. Stack gọi hàm để làm gì
Stack gọi hàm — là bạn thân khi debug lỗi phức tạp.
- Nó cho bạn thấy không chỉ chuyện gì đã xảy ra, mà còn ở đâu chính xác và ai là thủ phạm.
- Đôi khi nhìn vào stack, bạn sẽ bất ngờ không hiểu sao chương trình lại đi tới tình trạng đó (nhất là khi ai đó truyền nhầm giá trị argument).
Câu chuyện quen thuộc: Giả sử bạn có một project to đùng, các method gọi nhau cả chục tầng. Đột nhiên xuất hiện NullReferenceException, mà bạn không hiểu sao chương trình lại tới đó. Mở stack ra — thấy nguyên chuỗi gọi hàm, và thế là dễ biết bắt đầu tìm ở đâu.
Ví dụ:
class MyClass
{
public void MethodA() { MethodB(); }
public void MethodB() { MethodC(); }
public void MethodC() {
throw new Exception("Lỗi!");
}
public void Main()
{
try
{
MethodA();
}
catch (Exception ex)
{
Console.WriteLine(ex.StackTrace);
}
}
}
3. Cách tự tạo exception riêng
Đôi khi exception chuẩn là chưa đủ
Trong .NET có cả đống exception chuẩn (ArgumentNullException, InvalidOperationException v.v...), nhưng đôi khi vẫn chưa đủ:
- App của bạn có "luật chơi" riêng: ví dụ user không được mua quá 10 món một lần, hoặc trong business logic không được có số âm.
- Bạn muốn tách biệt lỗi logic app với lỗi "hệ thống".
Lúc này sẽ muốn tự tạo "của mình" — vì... được mà!
using System;
// Exception riêng: user không tìm thấy
public class UserNotFoundException : Exception
{
// constructor cơ bản
public UserNotFoundException() : base("User không tìm thấy.") { }
// constructor với message
public UserNotFoundException(string message) : base(message) { }
// constructor với message và exception lồng
public UserNotFoundException(string message, Exception inner) : base(message, inner) { }
}
Tóm tắt về constructor
- Không có tham số — sẽ dùng message mặc định
- Có message tự chọn — đôi khi muốn thêm chi tiết
- Có exception lồng (inner) — để nếu lỗi xảy ra "bên trong lỗi khác", không mất thông tin quan trọng.
Cách dùng exception riêng
Giả sử mình mô phỏng method tìm user trong app quản lý task:
using System;
public class UserService
{
public string FindUserNameById(int userId)
{
// "Tìm" user, nếu không thấy — ném exception
if (userId != 42)
throw new UserNotFoundException($"User với id {userId} không tìm thấy.");
return "Maksim";
}
}
Trong chương trình chính:
// Trong Main
var service = new UserService();
try
{
string name = service.FindUserNameById(17);
Console.WriteLine("Tên user: " + name);
}
catch (UserNotFoundException ex)
{
Console.WriteLine("Lỗi tìm user: " + ex.Message);
// Stack gọi hàm cũng có qua ex.StackTrace
}
Nếu truyền id khác 42, sẽ nhận được:
Lỗi tìm user: User với id 17 không tìm thấy.
4. Lý do nên tạo exception riêng
Log và phân loại lỗi
Giả sử app bạn có đủ loại lỗi, và cần xử lý khác nhau. Ví dụ, lỗi database thì log là "fatal", lỗi user thì hiện chữ đỏ cho user, lỗi mạng thì thử lại. Gom nhóm theo loại exception là cách hay để làm chuyện này.
OOP và kế thừa
Bạn có thể tổ chức hệ thống lỗi theo domain của mình:
public class MyAppException : Exception { ... }
public class OrderException : MyAppException { ... }
public class ProductException : MyAppException { ... }
public class TooManyItemsInOrderException : OrderException { ... }
Giờ, bắt MyAppException là xử lý được tất cả lỗi domain của bạn, còn muốn xử lý riêng "đơn hàng quá lớn" — bắt case cụ thể nhất.
5. Nhớ gì khi tạo exception riêng
- Đừng ném exception chỉ để cho có
Chỉ tạo exception riêng khi:- Thật sự giúp code dễ hiểu hơn
- Có khả năng sẽ được bắt từ code gọi
- Muốn truyền thêm thông tin cho code gọi (qua field/property)
- Best practice: Serialization
Trong .NET, exception chuẩn hỗ trợ serialization (để truyền qua mạng chẳng hạn). App nhỏ thì ít cần, nhưng case "pro" — thêm attribute [Serializable] và constructor cho serialization (xem tài liệu chính thức). Bài học thì khỏi làm, đi làm thì hỏi team lead nhé :)
Serialization — là quá trình chuyển object thành format tiện lưu trữ hoặc truyền (ví dụ file hoặc qua mạng). Sẽ học kỹ hơn về serialization sau.
6. Đặc điểm stack gọi hàm: dễ gây nhầm lẫn ở đâu
Stack gọi hàm chỉ cho thấy đường đi tới điểm xảy ra exception. Nếu bạn bắt exception rồi ném exception mới mà không truyền "exception lồng" (constructor Exception(string, Exception inner)), bạn có thể làm mất nguồn gốc lỗi. Cái này gọi là "che stack".
Xấu:
try
{
// Ở đây có lỗi gì đó
}
catch (Exception ex)
{
throw new Exception("Có lỗi không xác định."); // Stack lỗi trước bị mất!
}
Tốt:
try
{
// Ở đây có lỗi gì đó
}
catch (Exception ex)
{
throw new Exception("Có lỗi không xác định.", ex); // Giữ lại stack gốc!
}
Lúc này StackTrace sẽ còn cả đường đi tới lỗi đầu tiên và message mới của bạn.
7. Mẹo thực tế và lỗi thường gặp
- Đừng ném exception để điều khiển logic bình thường (ví dụ để "thoát khỏi vòng lặp" — có cách hay hơn nhiều!).
- Bắt đúng loại exception cần thiết — luôn bắt Exception không phải lúc nào cũng tốt (có thể "nuốt" lỗi mà bạn không nên).
- Luôn log stack gọi hàm khi gặp lỗi phức tạp. Sẽ cứu bạn rất nhiều khi debug bug.
- Dùng exception lồng — giúp không mất thông tin về nguyên nhân gốc.
- Viết exception của bạn rõ ràng — để sau này đọc lại code vẫn hiểu vì sao lỗi này xuất hiện.
GO TO FULL VERSION