1. The Problem
When using Tasks
on .net
platform, exception handling is one of most common problems for developers to deal with. In this blog, however, instead of talking about topics like how to properly propagate
exceptions in a task chain, we’ll only focus on how to gracefully handle exceptions (assume all exceptions are properly propagated).
The most common way is to call Task.ContinueWith
to create a continuation
for the task, then check for any exceptions when the task is completed and notify users in some way like displaying a MessageBox.
1
2
3
4
5
6
7
CallSomethingAsync().ContinueWith(t =>
{
if (t.Exception != null)
{
////Handle exceptions here
}
});
This is perfectly fine for small applications or some POC
, but it would likely cause maintenance headaches for large scale applications:
- To display a MessageBox when exceptions happen in a
Task
, extra dependencies are required,MessageBox service
for instance. - When
async
calls are many, the same piece of exception handling code need to be repeated as well as the dependencies for MessageBox.
We need some simpler way to handle the Task
exceptions gracefully.
Note that the
Task
here refers to thefire and forget
tasks like async calls invoked from event handlers ofButton.Click
.
2. Analysis
The TaskScheduler.UnobservedTaskException
event is one of the solutions for lazy developers. This event is used to receive task exceptions thrown but not observed, e.g. the Task.Exception
property has never been accessed by external codes. It works, but should not be the top one choice of responsible developers. On one hand, it leave the responsibilities of developers to CLR
, on the other hand, this event is only triggered when GC
happens, so it might cause miscommunications in production because the exceptions are actually delayed
to show to the user. The Unobserved
button in the sample code demonstrate this behavior。
In order to show the exceptions to the user with codes and dependencies as less as possible, the easiest way would be adding an extension method, the method in turn capture the exceptions and send them to someone
so they can be displayed.
1
CallSomethingAsync().WhenFaultedAsync("Failed to xxx");
And the someone
here is the subscribers of the Dispatcher.DispatcherUnhandledException
event. Not like the TaskScheduler.UnobservedTaskException
event, the former would trigger as long as the Dispatcher
flushes its priority queue.
3. Implementations
Talk is cheap, let’s show the code first. The WhenFaultedAsync
method accepts two parameters, a Dispatcher
and a string used to tell the context of the exception. Given that most of the applications only have one Dispatcher
, the second overload of WhenFaultedAsync
uses Application.Dispatcher
as default. For applications with more than one Dispatchers
, the first overload can be used to pass the current Dispatcher
if different from Application.Dispatcher
.
The core part of the code is to marshal to the Dispatcher
thread and throw an AggregateException
that contains the context so that is can be captured by the subscribers of the Dispatcher.DispatcherUnhandledException
event.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const TaskContinuationOptions FaultedFlag = TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously;
public static Task WhenFaultedAsync(this Task task, Dispatcher dispatcher, string context)
{
task.ContinueWith(t =>
{
if (t.Exception != null)
{
dispatcher.BeginInvoke(() => throw new AggregateException(context, t.Exception.InnerExceptions));
}
}, FaultedFlag);
return task;
}
public static Task WhenFaultedAsync(this Task task, string context)
{
return task.WhenFaultedAsync(Application.Current.Dispatcher, context);
}
Below is a sample to display a MessageBox
in the event handler. For cases of multiple exceptions being thrown in a short time, here are two possible strategies, details won’t be covered in this blog:
- One way is to show the first exception in a
MessageBox
and only log the rest of the exceptions before theMessageBox
is closed by the user. This is to avoid the famous annoying case of user seeing anotherMessageBox
after he closed one. - Alternatively, we can send all exceptions to a centralized place, e.g. an always live modeless window, and show them in a list/grid. So all of the exceptions can be presented to the user without interrupting the user’s current focus.
1
2
3
4
5
Application.Current.DispatcherUnhandledException += (_, e) =>
{
ShowException(e.Exception.Message, e.Exception);
e.Handled = true;
};
In certain cases we can even save the call to extension method like WhenFaultedAsync
. For example, in an application that uses Unity Container
, Interception
can be used to intercept all of the async calls of a class (usually a Service
impalements some interfaces) and automatically inject calls to the WhenFaultedAsync
extension method. This can help reduce some maintenance efforts for applications with clear architectures
Please visit github for more details.
1. 问题
异常处理是在.net
平台上使用Task
时最常遇到的问题之一。异常处理本身是个不小的话题,本文不打算把焦点放在如何在一连串的Task Chain
中正确Propagate
异常之类的话题,只讨论如何优雅地处理一个异步调用(可能包含一连串的Task
)中可能发生的异常(假设所有异常都会被正确地送回来)。
原生的做法是调用ContinueWith
来创建一个Continuation
,当任务结束后在其中判断是否有Exception
发生并处理之——比如显示一个MessageBox
告诉用户有错误发生。
1
2
3
4
5
6
7
CallSomethingAsync().ContinueWith(t =>
{
if (t.Exception != null)
{
////在这里处理异常
}
});
这样做在小型应用程序或某个POC
中完全正常,但是对于颇具规模的中大型应用程序中却会导致维护上的麻烦:
- 异常发生的时候如果要显示
MessageBox
则需要引入额外的依赖,比如某个MessageBox service
。 - 异步调用数量很多的时候在每个地方都需要重复同样的代码以及引入额外的依赖。
我们需要一种简洁而优雅的方式来处理Task
的异常。
注:此处
Task
特指fire and forget
的那种任务,比如在一个按钮的Click
事件里发起一个异步调用,上下文本身已有对Task
异常有处理的情况不在讨论之列。
2. 分析
一种偷懒的方式是使用TaskScheduler.UnobservedTaskException
事件——该事件用于接收任何在Task
中发生但未被观察到的异常,即Task.Exception
属性从未被外部代码显示访问过。这种方法可行但属于不负责任的做法,一方面把原本该由开发者捕获并做出相应处理的异常丢给CLR
,另一方面该事件只有等GC
发生的时候才会触发,实时性差,在生产环境中容易引起误解。本文例子中的Unobserved
按钮演示了这一行为。
针对我们的需求:用尽量简洁以及依赖最少的代码来把异常显示出来,能想到最简单的方式莫过于添加一个扩展方法,并让该方法把异常传播到合适的地方以便显示出来。
1
CallSomethingAsync().WhenFaultedAsync("xxx调用失败。");
而这个合适的地方,则是Dispatcher.DispatcherUnhandledException
事件的订阅者。跟TaskScheduler.UnobservedTaskException
事件不同的是,前者在Dispatcher
空闲的时候即会触发。
3. 实现
废话少说,先上代码。WhenFaultedAsync
接受两个参数,一是适当的Dispatcher
,二是一个用于区分异常来源的字符串。考虑到绝大多数应用程序实际上只有一个Dispatcher
,另一个重载则默认使用Application.Dispatcher
。有多个Dispatcher
的情形可以选择性调用第一个方法把Dispatcher
显示传递过去。
代码的重点在于异常发生时切换到Dispatcher
所在线程抛出一个包含上下文信息的AggregateException
,以便被Dispatcher.DispatcherUnhandledException
捕捉到。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const TaskContinuationOptions FaultedFlag = TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously;
public static Task WhenFaultedAsync(this Task task, Dispatcher dispatcher, string context)
{
task.ContinueWith(t =>
{
if (t.Exception != null)
{
dispatcher.BeginInvoke(() => throw new AggregateException(context, t.Exception.InnerExceptions));
}
}, FaultedFlag);
return task;
}
public static Task WhenFaultedAsync(this Task task, string context)
{
return task.WhenFaultedAsync(Application.Current.Dispatcher, context);
}
用于显示异常的代码可以是个简单的事件处理方法。当然这里有所简化,对于多个异常连续发生的情形有两个可用的策略,具体实现不再赘述:
- 其一,显示第一个异常为
MessageBox
,在MessageBox
被用户关掉之前,后续的异常只写入日志。避免出现用户关掉一个MessageBox
又来一个的尴尬场景。 - 其二,把异常全部发送到某个一直存在于内存中会自动弹出的非模态窗口,显示为列表或表格,一来所有异常一目了然,二来避免打断用户的当前操作。
1
2
3
4
5
Application.Current.DispatcherUnhandledException += (_, e) =>
{
ShowException(e.Exception.Message, e.Exception);
e.Handled = true;
};
在某些情况下下我们甚至可以省去WhenFaultedAsync
这样的扩展方法。比如一个使用了Unity Container
的系统可以通过Interception
截获某个类(通常是实现了某些接口的Service
)的所有异步调用,并动态生成代码来隐式调用WhenFaultedAsync
,这对于有清晰层级划分的系统来说可以一定程度简化应用层代码的维护。
完整实现请移步github