CodeGym /课程 /C# SELF /多个异常: AggregateException

多个异常: AggregateException

C# SELF
第 61 级 , 课程 1
可用

1. 介绍

想象一下:你同时启动十几个异步任务 — 爬网站、查电商价格或计算大数据。所有任务都嗖地启动了,但有几项失败了:有的服务器没响应,有的数据有问题。在同步代码里你通常只会看到一个异常,但在异步场景可能会有多个错误。这时候就轮到 AggregateException 上场了,它把所有错误装进一个“容器”里。

什么是 AggregateException

AggregateException 是 .NET 里的一个特殊异常类型,用来聚合来自并行/异步操作的错误:比如 Task.WhenAll, Parallel.For, TPL 等。当两个或更多任务以错误结束时,它们会被“打包”进一个 AggregateException 对象。

在哪会出现 AggregateException

最常见的场景是通过 Task.WhenAll 等待多个任务:

// 示例 — 并行运行多个任务,其中一些会抛出异常
var tasks = new List<Task>
{
    Task.Run(() => throw new InvalidOperationException("错误 #1")),
    Task.Run(() => throw new ArgumentException("错误 #2")),
    Task.Run(() => { Console.WriteLine("任务3执行成功!"); })
};

try
{
    await Task.WhenAll(tasks); // 在 await 这里 AggregateException 会被“展开”
}
catch (Exception ex)
{
    Console.WriteLine($"发生错误: {ex.GetType().Name} - {ex.Message}");
}

但有个细节:在使用 await 时,AggregateException 会被“展开”,直接抛出第一个内部异常“原样”。如果不使用 await,而是访问 Result 或调用 Wait(),你会得到真正的 AggregateException

2. AggregateException 的结构

这个异常包含一个集合 InnerExceptions — 所有的内层错误:

try
{
    Task task = Task.WhenAll(tasks);
    task.Wait(); // 如果有错误,这里会抛出 AggregateException
}
catch (AggregateException aggEx)
{
    Console.WriteLine($"错误总数: {aggEx.InnerExceptions.Count}");
    foreach (var e in aggEx.InnerExceptions)
    {
        Console.WriteLine($"类型: {e.GetType().Name}, 消息: {e.Message}");
    }
}

示例输出:

错误总数: 2
类型: InvalidOperationException, 消息: 错误 #1
类型: ArgumentException, 消息: 错误 #2

AggregateException 是怎么产生的


+--------------------------+
| 启动多个 Task           |
+-----+---------+----------+
      |         |
      v         v
  Task1    Task2 ... TaskN
   |          |
   |  (出错)
   |-------------------+
   |                   v
   |              Exception2
   v
Exception1

   |
   v
+---------------------------------+
| Task.WhenAll 或 Parallel.For    |
|        (收集所有异常)           |
+---------------------------------+
              |
              v
     AggregateException
   (InnerExceptions: 所有错误)

为什么用 await 看不到 AggregateException?

await 时,.NET 会把 AggregateException “展开”,抛出第一个内部异常(InnerExceptions[0])。

// 只有一个抛出 InvalidOperationException 的任务
try
{
    await Task.Run(() => throw new InvalidOperationException("出错!"));
}
catch (Exception ex)
{
    // 这里 ex 是 InvalidOperationException,而不是 AggregateException
    Console.WriteLine(ex.GetType().Name); // InvalidOperationException
}

想要访问所有错误的话——可以使用 Wait()Result(但要记住会阻塞线程)。

3. 捕获并处理所有异常

把理论和实践联系起来。假设有几个任务在处理用户数据,你需要记录所有失败情况。

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("操作不正确!")),
                Task.Run(() => throw new DivideByZeroException("除以零!")),
                Task.Run(() => Console.WriteLine("第三个任务成功完成"))
            };

            try
            {
                // 异步等待任务 — 会得到第一个内部异常
                Task allTasks = Task.WhenAll(tasks);
                await allTasks; // 在 await 时 AggregateException 会被展开
            }
            catch (Exception ex)
            {
                // 在 await 时我们只会看到第一个内部异常
                Console.WriteLine($"第一个错误: {ex.GetType().Name} - {ex.Message}");

                // 如果这是 AggregateException — 可以遍历所有内部异常
                if (ex is AggregateException aggEx)
                {
                    foreach (var inner in aggEx.InnerExceptions)
                    {
                        Console.WriteLine($"列表中的错误: {inner.GetType().Name} - {inner.Message}");
                    }
                }
            }

            // 现在同步地捕获所有错误
            try
            {
                var allTasks = Task.WhenAll(tasks);
                allTasks.Wait(); // 阻塞线程 — 在 UI 中别这样做!
            }
            catch (AggregateException aggEx)
            {
                foreach (var e in aggEx.InnerExceptions)
                {
                    Console.WriteLine($"[Wait] 错误: {e.GetType().Name} — {e.Message}");
                }
            }
        }
    }
}

使用 awaittry/catch 中你只会看到第一个错误。使用 Wait() 则会得到包含所有内部错误的传统 AggregateException

4. 大规模错误处理

在真实应用中常常需要对大量错误进行不同处理。比如你给所有用户群发邮件,有一部分发送失败:

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("发送邮件错误: " + ex.Message);
        else
            Console.WriteLine("未知错误: " + ex.Message);
    }
}

这样你就能记录每一次失败,并采取针对性的措施。

5. 有用的细节

这种行为的原因

.NET 的设计考虑的是方便性:在 UI 场景下通常第一条错误就够用了(在使用 await 时)。但在服务器或需要处理所有失败的系统里,AggregateException 是不可或缺的。

模式: Flatten — 展平错误列表

方法 Flatten() 会把嵌套的 AggregateException 展平成一个扁平的列表:

catch (AggregateException aggEx)
{
    foreach (var ex in aggEx.Flatten().InnerExceptions)
    {
        Console.WriteLine($"展开后的错误: {ex.GetType().Name} — {ex.Message}");
    }
}

不同等待方式下 Task 的行为

如何等待任务 会捕获到什么异常
await Task.WhenAll()
第一个内部错误
Task.WhenAll().Wait()
总是 AggregateException
Task.WhenAll().Result
总是 AggregateException
单独的 await 任务抛出的异常,或当任务以 AggregateException 结束时的第一个内部错误

需要记住

  • AggregateException 在可能同时出现多个错误的场景里很有用。
  • await 来说抓第一个错误更方便;对同步等待来说遍历所有 InnerExceptions 更合适。
  • 使用 Flatten()Handle() 可以更优雅地处理。
  • 不要在 UI 中混合同步和异步等待:会导致界面卡顿和死锁。

7. 处理 AggregateException 时常见的误区

误区:await Task.WhenAll 总是抛出 AggregateException。实际上不是,异常会被展开,你会看到第一个内部异常。

访问 Result 或调用 Wait() 会阻塞线程直到任务完成。在 UI 中这会造成界面卡顿。

如果你想“悄悄”处理一部分错误,可以使用 Handle()

catch (AggregateException aggEx)
{
    aggEx.Handle(ex =>
    {
        if (ex is InvalidOperationException)
        {
            // 这个错误我们已经处理了,可以不再抛出
            return true;
        }
        return false; // 其它异常会被重新抛出
    });
}
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION