Exception trong C#

Một ứng dụng có thể gặp lỗi trong quá trình thực thi. Khi xảy ra lỗi, Common Language Runtime (CLR) hoặc mã chương trình sẽ ném ra một exception (ngoại lệ) chứa thông tin cần thiết về lỗi.

Có hai loại exception trong .Net, ngoại lệ được tạo bởi chương trình thực thi và ngoại lệ được tạo bởi CLR.

C# cung cấp các lớp tích hợp cho mọi ngoại lệ có thể. Tất cả các lớp ngoại lệ được kế thừa trực tiếp hoặc gián tiếp từ lớp Exception.

Sau đây là hệ thống phân cấp của một số lớp ngoại lệ trong .Net:

Exception trong C#

Như bạn có thể thấy trong hình trên, lớp SystemException là lớp cơ sở cho tất cả các ngoại lệ có thể xảy ra trong quá trình thực thi chương trình.

Hình dưới đây cho thấy cách NullReferenceException được ném trong chế độ Debug của Visual Studio, khi nó truy cập vào một thuộc tính của một đối tượng null khi chạy:

Exception trong Visual Studio

Các lớp Exception quan trọng:

Bảng sau liệt kê các lớp Exception quan trọng có sẵn trong .Net.

Lớp exception Miêu tả
ArgumentException Xảy ra khi một đối số không null được truyền cho một phương thức không hợp lệ.
ArgumentNullException Xảy ra khi một đối số null được truyền cho một phương thức.
ArgumentOutOfRangeException Xảy ra khi giá trị của một đối số nằm ngoài phạm vi của các giá trị hợp lệ.
DivideByZeroException Xảy ra khi một giá trị nguyên được chia cho 0.
FileNotFoundException Xảy ra khi một file vật lý không tồn tại ở vị trí được chỉ định.
FormatException Xảy ra khi một giá trị không ở định dạng thích hợp được chuyển đổi từ chuỗi bằng phương thức chuyển đổi, chẳng hạn như Parse.
IndexOutOfRangeException Xảy ra khi một chỉ số mảng nằm ngoài giới hạn dưới hoặc trên của một mảng hoặc collection.
UnlimitedOperationException Xảy ra khi gọi một phương thức không hợp lệ trong trạng thái hiện tại của đối tượng.
UnlimitedCastException Xảy ra khi ép kiểu các kiểu dữ liệu không phù hợp.
KeyNotFoundException Xảy ra khi khóa được chỉ định để truy cập thành viên trong collection không tồn tại.
NotSupportedException Xảy ra khi một phương thức hoặc hoạt động không được hỗ trợ.
NullReferenceException Xảy ra khi chương trình truy cập thành viên của đối tượng null.
OverflowException Xảy ra khi hoạt động tính toán, ép kiểu hoặc chuyển đổi dẫn đến tràn bộ nhớ.
OutOfMemoryException Xảy ra khi một chương trình không có đủ bộ nhớ để thực thi mã.
StackOverflowException Xảy ra khi một ngăn xếp trong bộ nhớ tràn.
TimeoutException Xảy ra khi khoảng thời gian được phân bổ cho một hoạt động đã hết hạn.

Các lớp exception trong .Net đều dẫn xuất từ lớp Exception. Nó có các thuộc tính quan trọng mà bạn có thể sử dụng để lấy thông tin về ngoại lệ khi bạn xử lý ngoại lệ.

Thuộc tính Miêu tả
Message Cung cấp thông tin chi tiết về nguyên nhân xảy ra ngoại lệ.
StackTrace Cung cấp thông tin về nơi xảy ra lỗi.
InnerException Cung cấp thông tin về danh sách các ngoại lệ đã xảy ra.
HelpLink Thuộc tính này có thể lưu trữ URL trợ giúp cho một ngoại lệ cụ thể.
Data Thuộc tính này có thể lưu trữ dữ liệu tùy ý trong các cặp khóa-giá trị.
TargetSite Cung cấp tên của phương thức mà ngoại lệ này được đưa ra.

Khi xảy ra lỗi, mã ứng dụng hoặc trình xử lý mặc định sẽ ném ra các ngoại lệ. Tìm hiểu làm thế nào để xử lý các ngoại lệ trong phần tiếp theo của hướng dẫn này.

Những điểm cần nhớ:

  • Exception là lớp cơ sở cho bất kỳ kiểu ngoại lệ nào trong C#.
  • Có hai kiểu ngoại lệ chính là: SystemException và ApplicationException.
  • Lớp SystemException được sử dụng cho các lỗi thời gian chạy liên quan đến CLR.
  • Lớp Exception có các thuộc tính quan trọng như: Message, StackTrace, InnerException, Data, vv để xử lý ngoại lệ.

Xử lý Exception trong C#

Một exception (ngoại lệ) được CLR (Common Language Runtime) hoặc mã chương trình ném ra nếu có lỗi trong chương trình. Những ngoại lệ này cần được xử lý để ngăn chặn chương trình hiển thị thông báo lỗi cho người dùng và dừng thực thi.

C# cung cấp hỗ trợ để xử lý ngoại lệ bằng cách sử dụng khối try catch finally.

try
{
    // code that may raise exceptions
}
catch(Exception ex)
{
    // handle exception
}
finally
{
    // final cleanup code
}

Theo cú pháp trên, đặt mã có thể đưa ra một ngoại lệ vào khối try theo sau là một khối catch hoặc finally. Đưa các khai báo biến ra ngoài khối try để chúng có thể được truy cập trong khối catch và khối finally.

class Program
{
    static void Main(string[] args)
    {
        Console.Write("Enter Student Name: ");

        string studentName = Console.ReadLine();

        IList<string> studentList = FindAllStudentFromDatabase(studentName);

        Console.WriteLine("Total {0}: {1}", studentName, studentList.Count());

        Console.ReadKey();
    }

    private static IList<string> FindAllStudentFromDatabase(string studentName)
    {
        var studentList = // find all students with same name from the database 

        return studentList;
    }
}

Ví dụ trên hiển thị tổng số sinh viên có cùng tên. Giả sử rằng phương thức FindAllStudentFromDatabase() lấy danh sách sinh viên có cùng tên từ cơ sở dữ liệu.

Ví dụ trên sẽ hoạt động tốt nếu có ít nhất một sinh viên được tìm thấy, ngược lại nó sẽ gây ra ngoại lệ NullReferenceException.

Chúng ta không muốn chương trình hiển thị thông báo lỗi cho người dùng và dừng thực thi. Vì vậy, cần xử lý ngoại lệ bằng cách sử dụng khối try catch như dưới đây.

class Program
{
    static void Main(string[] args)
    {
        Console.Write("Enter Student Name: ");

        string studentName = Console.ReadLine();
        
        try
        {
            IList<string> studentList = FindAllStudentFromDatabase(studentName);

            Console.WriteLine("Total {0}: {1}", studentName, studentList.Count());
        }
        catch(Exception ex)
        {
            Console.Write("No Students exists for the specified name.");
        }

        Console.ReadKey();
    }

    private static IList<string> FindAllStudentFromDatabase(string studentName)
    {
        var studentList = // find all students with same name from the database 

        return studentList;
    }
}

Như bạn có thể thấy trong ví dụ trên, studentList.Count() có thể đưa ra một ngoại lệ nếu studentList null. Vì vậy, hãy bọc mã này vào trong khối try.

Khối try chỉ đơn giản nói cho trình biên dịch biết cần phải theo dõi ngoại lệ nếu nó xảy ra. Nếu ngoại lệ xảy ra trong khối try thì nó phải được xử lý bằng cách sử dụng khối catch.

Lưu ý: khối try phải được theo sau bởi khối catch hoặc finally hoặc cả hai. Khối try mà không có khối catch hoặc finally sẽ có lỗi khi biên dịch.

Khối catch trong C#

Ngoại lệ xảy ra trong khối try có thể được xử lý bằng cách sử dụng khối catch như trong ví dụ trên. Mã trong khối catch sẽ chỉ thực thi khi xảy ra ngoại lệ.

Trong C#, một khối try sẽ có một hoặc nhiều khối catch kèm theo để xử lý các ngoại lệ khác nhau.

Việc sử dụng nhiều khối catch sẽ rất hữu ích khi bạn muốn xử lý các ngoại lệ khác nhau theo những cách khác nhau.

class Program
{
    static void Main(string[] args)
    {
        Console.Write("Please enter two numbers: ");
        
        try
        {
            int num1 = int.Parse(Console.ReadLine());
            int num2 = int.Parse(Console.ReadLine());

            int result = num1 / num2;

            Console.WriteLine("{0} / {1} = {2}", num1, num2, result);
        }
        catch(DivideByZeroException ex)
        {
            LogError(ex);
            Console.Write("Cannot divide by zero. Please try again.");
        }
        catch(InvalidOperationException ex)
        {
            LogError(ex);
            Console.Write("Not a valid number. Please try again.");
        }
        catch(FormatException  ex)
        {
            LogError(ex);
            Console.Write("Not a valid number. Please try again.");
        }

        Console.ReadKey();
    }

}

Trong ví dụ trên, chúng tôi đã định nghĩa nhiều khối catch để xử lý các loại ngoại lệ khác nhau, để chúng tôi có thể hiển thị thông báo phù hợp cho người dùng. Điều này tùy thuộc vào loại lỗi và để người dùng tránh không lặp lại lỗi tương tự.

Lưu ý: Không được phép sử dụng nhiều khối catch cho cùng một loại ngoại lệ. Nó sẽ gây lỗi khi biên dịch.

Khối catch không hợp lệ

Khối catch{ } hoặc catch(Exception ex){ } không được phép tồn tại trong cùng một khối lệnh try catch, vì cả hai đều làm cùng một việc.

try
{
    //code that may raise an exception
}
catch //cannot have both catch and catch(Exception ex)
{ 
    Console.WriteLine("Exception occurred");
}
catch(Exception ex) //cannot have both catch and catch(Exception ex)
{
    Console.WriteLine("Exception occurred");
}

Ngoài ra, khối catch{ } hoặc catch(Exception ex){ } phải ở vị trí cuối cùng. Trình biên dịch sẽ báo lỗi nếu bạn có các khối catch khác nằm sau khối catch{ } hoặc catch(Exception ex).

try
{
    //code that may raise an exception
}
catch
{ 
    // this catch block must be last block
}
catch (NullReferenceException nullEx)
{
    Console.WriteLine(nullEx.Message);
}
catch (InvalidCastException inEx)
{
    Console.WriteLine(inEx.Message);
}

Khối finally trong C#

Khối finally phải đặt sau một khối try hoặc catch. Khối finally sẽ luôn được thực thi cho dù có xảy ra ngoại lệ hay không.

Khối finally thường được sử dụng để đóng kết nối, xóa dữ liệu tạm, hủy đối tượng, ...

static void Main(string[] args)
{
    int zero = 0;    
    
    try
    {
        int result = 5/zero;  // this will throw an exception       
    }
    catch(Exception ex)
    {
        Console.WriteLine("Inside catch block. Exception: {0}", ex.Message );
    }
    finally
    {
        Console.WriteLine("Inside finally block.");
    }
}

Kết quả khi chạy chương trình:

Inside catch block. Exception: Attempted to divide by zero.
Inside finally block.
Lưu ý: Chỉ có duy nhất một khối finally trong khối try catch finally. Ngoài ra, khối  finally không thể có các từ khóa return, continue, break. Nó không cho phép tự ý rời khỏi khối finally.

Khối try catch lồng nhau trong C#

C# cho phép các khối try catch lồng nhau. Trong khối try catch lồng nhau, một ngoại lệ sẽ được bắt và xử lý trong khối catch theo sau khối try nơi xảy ra ngoại lệ.

static void Main(string[] args)
{
    Student std = null;
           
    try
    {
        try
        {
            std.StudentName = "";
        }
        catch
        {
            Console.WriteLine("Inner catch");
        }
    }
    catch
    {
        Console.WriteLine("Outer catch");
    }
}

Kết quả khi chạy chương trình:

Inner catch

Nếu không có bất kỳ khối catch bên trong nào có kiểu ngoại lệ phù hợp, thì ngoại lệ sẽ chuyển sang khối catch bên ngoài cho đến khi tìm thấy bộ lọc ngoại lệ phù hợp.

static void Main(string[] args)
{
    Student std = null;
           
    try
    {
        try
        {
            // following throws NullReferenceException
            std.StudentName = "";
        }
        catch (InvalidOperationException innerEx)
        {
            Console.WriteLine("Inner catch");
        }
    }
    catch
    {
        Console.WriteLine("Outer catch");
    }
}

Kết quả khi chạy chương trình:

Outer catch

Trong ví dụ trên, std.StudentName sẽ ném ra ngoại lệ NullReferenceException, nhưng không có bất kỳ khối catch nào xử lý ngoại lệ NullReferenceException hoặc Exception. Vì vậy, nó sẽ được xử lý bởi khối catch bên ngoài.

Từ khóa throw trong C#

Một ngoại lệ có thể được ném ra bằng cách sử dụng từ khóa throw. Bất kỳ kiểu ngoại lệ nào kế thừa từ lớp Exception đều có thể được ném ra bằng cách sử dụng từ khóa throw.

static void Main(string[] args)
{
    Student std = null;

    try
    {
        PrintStudentName(std);
    }
    catch(Exception ex)
    {
        Console.WriteLine(ex.Message );
    }                      

    Console.ReadKey();
}

private static void PrintStudentName(Student std)
{
    if (std == null)
    {
        throw new NullReferenceException("Student object is null.");
    }
    Console.WriteLine(std.StudentName);
}

Đây là kết quả khi biên dịch và chạy chương trình:

Student object is null.

Trong ví dụ trên, phương thức PrintStudentName() ném ra ngoại lệ NullReferenceException nếu đối tượng std null.

Xin lưu ý rằng throw tạo ra một đối tượng thuộc bất kỳ kiểu ngoại lệ hợp lệ nào bằng cách sử dụng từ khóa new. Từ khóa throw không thể được sử dụng với bất kỳ kiểu dữ liệu nào khác không xuất phát từ lớp Exception.

Ném lại một ngoại lệ

Bạn cũng có thể ném lại một ngoại lệ từ khối catch để chuyển cho trình xử lý ngoại lệ phù hợp mà bạn muốn.

static void Main(string[] args)
{
    try
    {
        Method1();
    }
    catch(Exception ex)
    {
        Console.WriteLine(ex.StackTrace);
    }                      
}

static void Method1()
{
    try
    {
        Method2();
    }
    catch(Exception ex)
    {
        throw;
    } 
}

static void Method2()
{
    string str = null;
    try
    {
        Console.WriteLine(str[0]);
    }
    catch(Exception ex)
    {
        throw;
    }
}

Đây là kết quả khi chạy chương trình trên:

at Program.Method2() in d:\Windows\Temp\kz1lzfqu.0.cs:line 40
at Program.Method1() in d:\Windows\Temp\kz1lzfqu.0.cs:line 27
at Program.Main() in d:\Windows\Temp\kz1lzfqu.0.cs:line 11

Trong ví dụ trên, một ngoại lệ xảy ra trong phương thức Method2(). Khối catch sẽ ném ngoại lệ đó bằng cách chỉ sử dụng từ khóa throw (không có e).

Ngoại lệ này sẽ được xử lý trong khối catch của phương thức Method1(), phương thức này lại tiếp tục ném ngoại lệ và cuối cùng nó được xử lý trong khối catch của phương thức Main().

Dấu vết ngăn xếp (StackTrace) của ngoại lệ này sẽ cung cấp cho bạn chi tiết đầy đủ về nơi chính xác ngoại lệ này xảy ra.

Nếu bạn ném lại một ngoại lệ bằng tham số ngoại lệ thì nó sẽ không giữ ngoại lệ ban đầu mà tạo ngoại lệ mới.

static void Main(string[] args)
{
    try
    {
        Method1();
    }
    catch(Exception ex)
    {
        Console.WriteLine(ex.StackTrace);
    }                      
}

static void Method1()
{
    try
    {
        Method2();
    }
    catch(Exception ex)
    {
        throw ex;
    } 
}

static void Method2()
{
    string str = null;
    try
    {
        Console.WriteLine(str[0]);
    }
    catch(Exception ex)
    {
        throw;
    } 
}

Đây là kết quả khi biên dịch và chạy chương trình:

at Program.Method1() in d:\Windows\Temp\xzsis2fq.0.cs:line 25
at Program.Main() in d:\Windows\Temp\xzsis2fq.0.cs:line 9

Trong ví dụ trên, ngoại lệ bắt được trong phương thức Main() sẽ hiển thị dấu vết ngăn xếp từ phương thức Method1 và phương thức Main.

Nó sẽ không hiển thị phương thức Method2 trong dấu vết ngăn xếp khi chúng ta ném lại ngoại lệ trong phương thức Method1() bằng cách sử dụng throw ex. Vì vậy, không bao giờ ném một ngoại lệ bằng cách sử dụng throw <exception parameter>.

Tạo lớp Exception tùy chỉnh trong C#

Bạn cũng có thể tự định nghĩa các lớp ngoại lệ của riêng bạn. Các lớp ngoại lệ do người dùng định nghĩa nên được kế thừa từ lớp Exception (theo khuyến nghị của Microsoft).

using System;

namespace UserDefinedException 
{
   class TestTemperature 
   {
      static void Main(string[] args) 
      {
         Temperature temp = new Temperature();
         try 
         {
            temp.ShowTemp();
         } 
         catch(TempIsZeroException e) 
         {
            Console.WriteLine("TempIsZeroException: {0}", e.Message);
         }
         Console.ReadKey();
      }
   }
}

public class TempIsZeroException: Exception 
{
   public TempIsZeroException(string message): base(message) 
   {
   }
}

public class Temperature 
{
   int temperature = 0;
   
   public void ShowTemp() 
   {      
      if(temperature == 0) 
      {
         throw new TempIsZeroException("Zero Temperature found");
      } 
      else 
      {
         Console.WriteLine("Temperature: {0}", temperature);
      }
   }
}

Khi đoạn mã trên được biên dịch và thực thi, nó tạo ra kết quả sau:

TempIsZeroException: Zero Temperature found

Những điểm cần nhớ:

  • Sử dụng các khối try, catch finally để xử lý các ngoại lệ trong C#.
  • Khối try phải được theo sau bởi một khối catch hoặc finally hoặc cả hai.
  • Có thể sử dụng nhiều khối catch để bắt và xử lý nhiều loại ngoại lệ khác nhau. Khối catch{ } hoặc catch(Exception ex){ } phải ở cuối cùng.
  • Không thể sử dụng đồng thời hai khối catch{ }catch(Exception ex){ }.
  • Khối finally phải ở sau khối try hoặc catch.
  • Khối finally sẽ luôn được thực thi bất kể có xảy ra exception hay không.
  • Khối finally là nơi thích hợp để xử lý các đối tượng như đóng kết nối, xóa bộ nhớ đệm, hủy đối tượng, ...
  • Khối finally không thể sử dụng từ khóa return, continue, break vì nó không cho phép tự ý rời khỏi khối này.
  • Được phép sử dụng các khối try catch lồng nhau trong C#.
  • Một ngoại lệ sẽ được bắt và xử lý trong khối catch bên trong nếu tìm thấy bộ lọc thích hợp, nếu không nó sẽ bị bắt và xử lý bởi khối catch bên ngoài.
  • Sử dụng throw thay vì throw ex để giữ stack trace giúp truy vết và xử lý lỗi dễ dàng.
Lập Trình C#Lập Trình C# Cơ Bản
Bài Viết Liên Quan:
int[] và int[,] trong C#: Ai nhanh hơn
Trung Nguyen 10/10/2020
int[] và int[,] trong C#: Ai nhanh hơn

Hiểu được sự khác biệt giữa các loại mảng trong C# sẽ giúp bạn chọn cấu trúc dữ liệu chính xác cho mọi trường hợp.

Struct và class trong C#: Ai nhanh hơn
Trung Nguyen 09/10/2020
Struct và class trong C#: Ai nhanh hơn

Trong bài viết này, tôi sẽ so sánh sự khác biệt về hiệu suất giữa struct và class trong C#: Ai nhanh hơn.

Best practice cho performance trong C#
Trung Nguyen 03/10/2020
Best practice cho performance trong C#

Mục tiêu của bài viết này là cung cấp một danh sách không đầy đủ các code mẫu cần tránh, vì chúng rủi ro hoặc performance kém.

Đọc ghi file (File I/O) trong C#
Trung Nguyen 26/04/2020
Đọc ghi file (File I/O) trong C#

Hướng dẫn này sẽ giúp bạn tìm hiểu về đọc ghi file (File I/O) trong C# và sử dụng các lớp tiện ích để đọc ghi file.