Khi có gì đó sai lúc chạy (một tệp bị thiếu, một đoạn văn bản không phải số, một khóa không có trong dictionary), .NET ném ra một ngoại lệ (exception): một đối tượng mô tả lỗi. Ngoại lệ đi ngược lên call stack cho tới khi một khối catch xử lý nó. Nếu không có khối nào, chương trình dừng lại và in ra lỗi.
try catch cơ bản
Bọc phần code có thể gặp lỗi trong try, và xử lý lỗi trong catch:
Đầu ra:
42 doubled is 84
'forty-two' is not a number
7 doubled is 14
Still running
Khi int.Parse("forty-two") ném ngoại lệ, câu lệnh Console.WriteLine phía sau trong khối try bị bỏ qua, khối catch (FormatException) chạy, và vòng lặp tiếp tục. Bạn có thể bỏ biến (catch (FormatException)) khi không cần đến đối tượng ngoại lệ.
Riêng trường hợp này có công cụ tốt hơn: int.TryParse(input, out int n) trả về false thay vì ném ngoại lệ. Ngoại lệ dành cho những tình huống code không lường trước; đầu vào thường xuyên sai là chuyện đã lường trước, nên hãy kiểm tra nó thay vì bắt lỗi.
Ngoại lệ đi qua các phương thức thế nào
Một ngoại lệ được ném ở sâu trong chuỗi lời gọi sẽ tháo dần từng phương thức cho tới khi gặp trình xử lý phù hợp. Code phía sau lệnh ném trong mỗi phương thức đó không được chạy.
Đầu ra:
OrderTotal finished
5.00
Caught KeyNotFoundException in Main
PriceOf và OrderTotal không có catch, nên ngoại lệ đi thẳng qua chúng tới Main. Hãy đặt trình xử lý ở tầng biết phải làm gì với lỗi đó, mà tầng này thường không phải là nơi lỗi xảy ra.
Bắt từng ngoại lệ cụ thể, theo đúng thứ tự
Một mệnh đề catch xử lý kiểu của nó và mọi kiểu kế thừa từ kiểu đó. Khi có nhiều mệnh đề, mệnh đề khớp đầu tiên sẽ thắng, nên hãy sắp xếp chúng từ cụ thể nhất đến chung nhất:
Đầu ra:
10 / 2 = 5
Cannot divide by zero
Both values must be whole numbers
Unexpected: OverflowException
Lời gọi cuối cùng ném OverflowException, không mệnh đề cụ thể nào xử lý nó, nên catch (Exception e) chung sẽ nhận. Đặt catch (Exception) lên đầu sẽ khiến các mệnh đề phía sau không bao giờ tới được, và trình biên dịch từ chối điều đó bằng lỗi CS0160.
Exception filter với when
C# 6 thêm when: một điều kiện quyết định mệnh đề catch có áp dụng hay không. Nếu điều kiện là false, runtime tiếp tục tìm trình xử lý khác như thể mệnh đề đó không tồn tại.
Đầu ra:
Not found: show an empty page
Server error 503: retry later
Unhandled by Call: HTTP 401
Bộ lọc cho phép bạn rẽ nhánh theo dữ liệu bên trong ngoại lệ mà không cần bắt rồi ném lại. Đây cũng là cách gọn gàng để xử lý hai kiểu không liên quan theo cùng một cách, như mệnh đề thứ ba. Bộ lọc chạy trước khi stack bị tháo, nên khi không có bộ lọc nào khớp, debugger hoặc crash dump vẫn thấy được trạng thái ban đầu.
finally: code luôn được chạy
Khối finally chạy khi luồng điều khiển rời khỏi try, dù nó chạy xong, trả về sớm hay ném ngoại lệ. Đây là chỗ để dọn dẹp. (Có một lưu ý: nếu không ở đâu bắt ngoại lệ, tiến trình có thể kết thúc mà không chạy khối này.)
Đầu ra:
Open connection
Close connection
finished
Open connection
Close connection
returned early
Open connection
Close connection
handled error
Trong mọi trường hợp, "Close connection" đều được in ra trước khi kết quả của phương thức tới được Main: finally chạy sau khi giá trị của return đã được tính nhưng trước khi phương thức thật sự trả về. Một try có thể có finally mà không cần catch nào, để dọn dẹp trong khi vẫn để ngoại lệ tiếp tục đi tới nơi gọi.
Với các đối tượng cài đặt IDisposable (tệp, stream, kết nối), câu lệnh using sẽ viết sẵn cặp try/finally này cho bạn.
Ném lại: throw; và throw e;
Đôi khi một khối catch ghi log hoặc lưu lại điều gì đó rồi để ngoại lệ đi tiếp. Cách bạn ném lại quyết định stack trace có còn nguyên hay không:
Đầu ra:
throw; trace mentions LoadConfig: True
throw e; trace mentions LoadConfig: False
throw e; coi ngoại lệ như vừa được ném từ khối catch, nên các frame bên dưới, kể cả phương thức nơi lỗi xảy ra, biến mất khỏi trace. Luôn ném lại bằng throw; trơn. (Thuộc tính NoInlining có mặt chỉ vì JIT có thể gộp một phương thức nhỏ như vậy vào nơi gọi nó, khiến nó biến mất khỏi cả hai trace.)
Nếu muốn thêm ngữ cảnh, hãy bọc ngoại lệ trong một ngoại lệ mới và truyền ngoại lệ gốc vào làm inner exception: throw new ConfigException("Could not start the app", e);. Inner exception giữ stack trace riêng của nó, và các trình ghi log sẽ in ra cả chuỗi. Cách viết kiểu ngoại lệ của riêng bạn có ở trang ném ngoại lệ.
Đối tượng Exception
Mọi ngoại lệ đều kế thừa từ System.Exception. Các thành viên bạn dùng nhiều nhất:
| Thành viên | Chứa gì |
|---|---|
Message | Mô tả dễ đọc cho người |
GetType().Name | Kiểu ngoại lệ, ví dụ FormatException |
StackTrace | Chuỗi lời gọi phương thức tại điểm ném |
InnerException | Ngoại lệ đã gây ra ngoại lệ này, hoặc null |
ToString() | Kiểu, thông báo, các inner exception và stack trace gộp lại |
Hãy ghi log e.ToString() thay vì e.Message khi bạn muốn chẩn đoán lỗi về sau: riêng thông báo hiếm khi cho biết vấn đề nằm ở đâu. Đừng hiển thị e.ToString() cho người dùng cuối.
Các kiểu ngoại lệ thường gặp
| Ngoại lệ | Nguyên nhân thường gặp |
|---|---|
NullReferenceException | Gọi một thành viên trên tham chiếu null |
ArgumentNullException | Một phương thức nhận null ở chỗ cần có giá trị |
ArgumentOutOfRangeException | Một đối số hoặc chỉ số của list nằm ngoài khoảng cho phép |
IndexOutOfRangeException | Chỉ số mảng nằm ngoài giới hạn |
FormatException | int.Parse, DateTime.Parse và các hàm tương tự gặp văn bản sai định dạng |
InvalidCastException | Ép kiểu tường minh sang một kiểu mà đối tượng không thuộc về |
InvalidOperationException | Đối tượng đang ở trạng thái không phù hợp với lời gọi (dãy rỗng, collection bị sửa đổi) |
KeyNotFoundException | Đọc một khóa không tồn tại trong dictionary bằng indexer |
DivideByZeroException | Chia số nguyên hoặc decimal cho 0 |
OverflowException | Số được parse, phép chuyển đổi checked hoặc kết quả tính toán checked không vừa với kiểu |
FileNotFoundException, IOException | Vấn đề về hệ thống tệp |
NullReferenceException, IndexOutOfRangeException và InvalidCastException gần như luôn có nghĩa là có bug. Hãy sửa code thay vì bắt chúng.
Đừng nuốt ngoại lệ
Một khối catch rỗng che giấu mọi lỗi, kể cả những lỗi bạn không lường trước:
try
{
SaveOrder(order);
}
catch (Exception)
{
// nothing: the order silently was not saved
}
Chương trình tiếp tục chạy như thể việc lưu đã thành công, và nguyên nhân thật bị mất. Vài nguyên tắc giúp việc xử lý ngoại lệ minh bạch:
- Chỉ bắt những gì bạn xử lý được, ở tầng có thể xử lý nó.
- Nếu bắt để ghi log, hãy ném lại bằng
throw;trừ khi chương trình thật sự chạy tiếp được. - Chỉ bắt
Exceptionở rìa ngoài cùng:Main, một trình xử lý request, một vòng lặp worker. - Với những trường hợp đã lường trước, hãy ưu tiên
TryParse,TryGetValuevà kiểm tranullthay vì ngoại lệ. Ném ngoại lệ chậm hơn nhiều so với một phép kiểm tra, còn một khốitrykhông ném gì thì gần như không tốn chi phí.
Lỗi thường gặp
- Đặt
catch (Exception)lên đầu. Các mệnh đề phía sau không bao giờ tới được (CS0160). - Dùng
throw e;để ném lại. Nó xóa stack trace gốc; hãy dùngthrow;. - Khối catch rỗng. Lỗi biến mất; ít nhất hãy ghi log rồi ném lại.
- Dùng ngoại lệ để điều khiển luồng chạy. Kiểm tra đầu vào bằng
TryParsethay vì bắtFormatException. - Hiển thị
e.Messagecủa ngoại lệ từ framework cho người dùng. Câu chữ khác nhau giữa các phiên bản .NET và được viết cho lập trình viên.
Câu hỏi thường gặp
try catch trong C# hoạt động thế nào?
Code có thể gặp lỗi được đặt trong khối try. Nếu một câu lệnh trong đó ném ngoại lệ, phần còn lại của khối bị bỏ qua và runtime tìm một mệnh đề catch có kiểu khớp với ngoại lệ, trước tiên trong phương thức hiện tại rồi lần lượt ở từng nơi gọi. Khối catch khớp đầu tiên sẽ chạy, và chương trình tiếp tục sau toàn bộ câu lệnh try.
finally trong C# có luôn chạy không?
Gần như luôn luôn: sau khi khối try chạy xong bình thường, sau khi một catch xử lý ngoại lệ, sau một return hoặc break bên trong khối, và khi một ngoại lệ đi xuyên qua để tới một catch ở tầng trên của call stack. Nó không chạy khi tiến trình kết thúc trước: tiến trình bị kill, Environment.FailFast, một StackOverflowException, và trên .NET Core trở về sau là một ngoại lệ không có ai bắt, vốn làm tiến trình dừng trước khi các khối finally kịp chạy.
Làm sao bắt nhiều ngoại lệ trong C#?
Viết nhiều mệnh đề catch, kiểu cụ thể nhất đứng trước: catch (FileNotFoundException) trước catch (IOException), rồi mới đến catch (Exception). Trình biên dịch từ chối một mệnh đề không bao giờ tới được vì một mệnh đề trước đó đã bắt kiểu của nó. Để xử lý hai kiểu không liên quan theo cùng một cách, hãy dùng bộ lọc: catch (Exception e) when (e is FormatException || e is OverflowException).
throw và throw ex trong C# khác nhau thế nào?
Bên trong một catch, throw; ném lại ngoại lệ hiện tại cùng stack trace gốc của nó. throw ex; ném đúng đối tượng đó nhưng đặt lại stack trace về dòng hiện tại, nên phương thức nơi lỗi thật sự xảy ra biến mất khỏi trace. Hãy dùng throw;, hoặc bọc lại: throw new MyException("context", ex);.
Exception filter trong C# là gì?
Là một mệnh đề when đặt sau catch (từ C# 6): catch (HttpRequestException e) when (e.Message.Contains("404")). Khối catch chỉ chạy khi điều kiện là true; nếu không, ngoại lệ tiếp tục tìm trình xử lý khác như thể mệnh đề đó không tồn tại, và stack của nó không bị tháo.
Có nên bắt Exception trong C# không?
Chỉ ở rìa ngoài của chương trình: đầu hàm Main, một trình xử lý request, hoặc một vòng lặp chạy nền, nơi việc cần làm là ghi log lỗi rồi chạy tiếp hoặc thoát gọn gàng. Ở sâu bên trong code, hãy bắt những kiểu cụ thể mà bạn thật sự xử lý được, và để mọi thứ khác đi tiếp lên trên.