CodeGym /Các khóa học /C# SELF /Một số ngoại lệ: AggregateException

Một số ngoại lệ: AggregateException

C# SELF
Mức độ, Bài học
Có sẵn

1. Giới thiệu

Hãy tưởng tượng: bạn khởi chạy ngay hàng chục task bất đồng bộ — parse mấy trang web, kiểm tra giá trên marketplace hoặc tính toán trên mảng dữ liệu lớn. Mọi thứ khởi động tốt, nhưng vài task bị rớt lỗi: chỗ thì server không trả lời, chỗ thì dữ liệu có vấn đề. Trong code đồng bộ bạn sẽ thấy một ngoại lệ, còn trong bất đồng bộ — có thể có nhiều lỗi. Ở đây AggregateException xuất hiện, nó gom tất cả lỗi lại vào một "container".

AggregateException là gì?

AggregateException — một loại ngoại lệ đặc biệt trong .NET, gom các lỗi từ các thao tác song song/bất đồng bộ: Task.WhenAll, Parallel.For, TPL và những cái khác. Khi hai task trở lên kết thúc với lỗi, chúng được "đóng gói" vào một đối tượng AggregateException.

AggregateException xuất hiện ở đâu?

Kịch bản phổ biến nhất — chờ nhiều task qua Task.WhenAll:

// Ví dụ — gọi song song vài task, một số trong đó ném ngoại lệ
var tasks = new List<Task>
{
    Task.Run(() => throw new InvalidOperationException("Lỗi #1")),
    Task.Run(() => throw new ArgumentException("Lỗi #2")),
    Task.Run(() => { Console.WriteLine("Task 3 chạy thành công!"); })
};

try
{
    await Task.WhenAll(tasks); // Khi await ở đây AggregateException sẽ được "mở ra"
}
catch (Exception ex)
{
    Console.WriteLine($"Xảy ra lỗi: {ex.GetType().Name} - {ex.Message}");
}

Nhưng có một lưu ý: khi dùng await AggregateException sẽ được "mở ra", và ngoại lệ nội bộ đầu tiên sẽ bị ném ra "nguyên vẹn". Nếu không dùng await, mà truy cập Result hoặc gọi Wait(), bạn sẽ nhận được chính xác AggregateException.

2. AggregateException trông như thế nào?

Đây là ngoại lệ có một collection InnerExceptions — tất cả các lỗi lồng nhau:

try
{
    Task task = Task.WhenAll(tasks);
    task.Wait(); // Ở đây sẽ là chính xác AggregateException, nếu có lỗi
}
catch (AggregateException aggEx)
{
    Console.WriteLine($"Tổng số lỗi: {aggEx.InnerExceptions.Count}");
    foreach (var e in aggEx.InnerExceptions)
    {
        Console.WriteLine($"Loại: {e.GetType().Name}, Thông điệp: {e.Message}");
    }
}

Ví dụ đầu ra:

Tổng số lỗi: 2
Loại: InvalidOperationException, Thông điệp: Lỗi #1
Loại: ArgumentException, Thông điệp: Lỗi #2

Cách AggregateException phát sinh


+--------------------------+
| Khởi chạy nhiều Task     |
+-----+---------+----------+
      |         |
      v         v
  Task1    Task2 ... TaskN
   |          |
   |  (bị lỗi)
   |-------------------+
   |                   v
   |              Exception2
   v
Exception1

   |
   v
+---------------------------------+
| Task.WhenAll hoặc Parallel.For  |
|        (gom tất cả ngoại lệ)     |
+---------------------------------+
              |
              v
     AggregateException
   (InnerExceptions: tất cả lỗi)

Tại sao với await không thấy AggregateException?

Khi dùng await .NET "mở" AggregateException và ném ra ngoại lệ nội bộ đầu tiên (InnerExceptions[0]).

// Chỉ có một task ném InvalidOperationException
try
{
    await Task.Run(() => throw new InvalidOperationException("Lỗi!"));
}
catch (Exception ex)
{
    // Ở đây ex chính là InvalidOperationException, chứ không phải AggregateException
    Console.WriteLine(ex.GetType().Name); // InvalidOperationException
}

Muốn truy cập tất cả các lỗi — dùng Wait() hoặc Result (nhưng nhớ là sẽ block thread).

3. Bắt và xử lý tất cả ngoại lệ

Kết hợp lý thuyết với thực tế. Giả sử vài task xử lý dữ liệu người dùng, và bạn cần ghi lại tất cả thất bại.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace MyApp
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var tasks = new List<Task>
            {
                Task.Run(() => throw new InvalidOperationException("Không phải thao tác đúng!")),
                Task.Run(() => throw new DivideByZeroException("Chia cho 0!")),
                Task.Run(() => Console.WriteLine("Task thứ ba chạy thành công"))
            };

            try
            {
                // Chờ các task bất đồng bộ — nhận ngoại lệ nội bộ đầu tiên
                Task allTasks = Task.WhenAll(tasks);
                await allTasks; // khi await AggregateException sẽ được mở ra
            }
            catch (Exception ex)
            {
                // Khi await ta sẽ thấy ngoại lệ nội bộ đầu tiên
                Console.WriteLine($"Lỗi đầu tiên: {ex.GetType().Name} - {ex.Message}");

                // Nếu đó là AggregateException — có thể duyệt qua tất cả
                if (ex is AggregateException aggEx)
                {
                    foreach (var inner in aggEx.InnerExceptions)
                    {
                        Console.WriteLine($"Lỗi trong danh sách: {inner.GetType().Name} - {inner.Message}");
                    }
                }
            }

            // Bây giờ bắt tất cả lỗi theo cách đồng bộ
            try
            {
                var allTasks = Task.WhenAll(tasks);
                allTasks.Wait(); // block thread — đừng làm vậy trong UI!
            }
            catch (AggregateException aggEx)
            {
                foreach (var e in aggEx.InnerExceptions)
                {
                    Console.WriteLine($"[Wait] Lỗi: {e.GetType().Name} — {e.Message}");
                }
            }
        }
    }
}

Khi dùng await trong khối try/catch bạn sẽ chỉ thấy lỗi đầu tiên. Khi dùng Wait() sẽ nhận được AggregateException cổ điển với tất cả lỗi lồng nhau.

4. Xử lý hàng loạt lỗi

Trong ứng dụng thực tế thường cần xử lý nhiều lỗi khác nhau. Ví dụ, bạn gửi email tới tất cả người dùng, và một phần gửi bị lỗi:

var sendTasks = emailList.Select(email => Task.Run(() => SendEmail(email))).ToList();

try
{
    Task.WaitAll(sendTasks.ToArray());
}
catch (AggregateException aggEx)
{
    foreach (var ex in aggEx.InnerExceptions)
    {
        if (ex is SmtpException)
            Console.WriteLine("Lỗi gửi mail: " + ex.Message);
        else
            Console.WriteLine("Lỗi không xác định: " + ex.Message);
    }
}

Như vậy bạn ghi nhận từng thất bại và có thể thực hiện biện pháp riêng cho từng trường hợp.

5. Những lưu ý hữu ích

Nguyên nhân của hành vi này

.NET thiết kế theo sự tiện lợi: trong UI thường chỉ cần lỗi đầu tiên (khi dùng await). Nhưng trên server hoặc trong hệ thống cần xử lý tất cả thất bại thì không thể thiếu AggregateException.

Pattern: Flatten — "mở phẳng" danh sách lỗi

Phương thức Flatten() biến các AggregateException lồng nhau thành danh sách phẳng:

catch (AggregateException aggEx)
{
    foreach (var ex in aggEx.Flatten().InnerExceptions)
    {
        Console.WriteLine($"Lỗi đã được mở: {ex.GetType().Name} — {ex.Message}");
    }
}

Task có hành vi thế nào với các cách chờ khác nhau

Cách chờ task Ta sẽ bắt được ngoại lệ nào
await Task.WhenAll()
Lỗi nội bộ đầu tiên
Task.WhenAll().Wait()
Luôn luôn AggregateException
Task.WhenAll().Result
Luôn luôn AggregateException
Await riêng từng task Exception do task ném ra, hoặc lỗi nội bộ đầu tiên nếu task kết thúc với AggregateException

Quan trọng cần nhớ

  • AggregateException hữu ích trong mọi kịch bản có thể có nhiều lỗi đồng thời.
  • Với await tiện để bắt lỗi đầu tiên; với chờ đồng bộ — duyệt tất cả InnerExceptions.
  • Sử dụng Flatten() và Handle() để xử lý sạch sẽ.
  • Đừng trộn chờ đồng bộ và bất đồng bộ trong UI: sẽ gây treo và deadlock.

7. Những lỗi thường gặp khi xử lý AggregateException

Hiểu lầm: await Task.WhenAll luôn ném AggregateException. Thực tế — không, ngoại lệ được mở ra và bạn sẽ thấy ngoại lệ nội bộ đầu tiên.

Truy cập Result hoặc gọi Wait() sẽ block thread cho đến khi các task hoàn thành. Trong UI điều này làm giao diện bị treo.

Nếu bạn muốn xử lý một số lỗi "âm thầm", dùng Handle():

catch (AggregateException aggEx)
{
    aggEx.Handle(ex =>
    {
        if (ex is InvalidOperationException)
        {
            // Lỗi này chúng ta đã xử lý, không cần ném tiếp
            return true;
        }
        return false; // Các ngoại lệ khác sẽ bị ném lại
    });
}
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION