In continuing our look at Task, await, and asynchronous programming, we need to consider what happens if something goes wrong in the asynchronous method. Today we'll look at how we handle exceptions in the code that is consuming the asynchronous method.
We continue with code created in our previous samples. To get up to speed, check out the articles here: Exploring Task, Await, and Asynchronous Methods. The code is available on GitHub: https://github.com/jeremybytes/using-task.
The completed code for this article is in the 07-ExceptionBasics branch.
[Update 09/2015: A video version of this information is available here: Basic Exception Handling.]
Exception in the Asynchronous Method
The first thing we need to do is throw an exception in our asynchronous method. As a reminder, we're using the "Get" method of the "PersonRepository" class (from the UsingTask.Library project).
We'll add an exception like this:
We put the exception after the "Task.Delay" so that the exception will not be thrown until *after* our 3 second delay. This gives us a chance to see how things work when we call the code.
Now let's flip over to our WPF application (in the "UsingTask.UI" project) and see how it behaves.
Current Behavior with Task
We'll start by clicking the "Fetch Data (with Task)" button. Here's what happens when we click the button:
And after 3 seconds:
Hmm. The button disabled then re-enabled, but nothing else happened. Let's take a look at the code:
Now this makes sense. In our first continuation, we have a task continuation option specified: "OnlyOnRanToCompletion". That means that this block of code will not run because the task did not complete successfully.
In our second continuation, we reset the enabled state of our button. There are no continuation options on this, so it always runs. That's why our button gets re-enabled even when we get an exception.
So this behavior makes sense, but we probably want to give some more information to the user about what happened. We'll get to this in a bit. First, let's see how our "await" method behaves.
Current Behavior with "await"
When we click the "Fetch Data (with await)" button, we get a different result: a runtime error:
This shows that we have an unhandled exception. Let's review the code.
One of the great things about using "await" is that we can treat our code almost the same as if it were synchronous code. And if we were to assume that this was synchronous code, if "repository.Get()" throws an exception, then it would be unhandled.
We do have a "try" block, but no "catch" block to deal with an exception. We'll come back to this in a bit. First, let's see how we can deal with the exception in the Task method. This will let us understand a bit better what we get when we use "await".
Exception Handling with Task
In our Task method, we have a continuation that only runs if the task completes successfully. In the same way, we can add a continuation that only runs if the task throws an exception.
A task that throws an exception is referred to as being "faulted". Here's the shell of a continuation that we can use:
For this continuation, we have specified the option "OnlyOnFaulted". So this will run only if we get an exception in the task. Our other parameters will stay the same; we want to stay on the UI thread because we'll be outputting the errors to the UI.
Before we can deal with the exception, we need to take a look at the type of exception that we get from a task. We can see this by inspecting the "Exception" property of our task.
This shows that we have an "AggregateException" (as opposed to a standard "Exception"). An AggregateException can hold multiple exceptions. Let's create a variable for this.
Think about this: we can kick off multiple operations at the same time using a single Task. What if one of the operations fails? What if multiple operations fail? This is something that we need to take into consideration.
In this case, our library only runs a single operation on the Task, but as a consumer of the library, we don't know that. So we need to be prepared for the potential of multiple exceptions.
Turtles All the Way Down
But how do we get to the "real" exception? The "AggregateException" has a property called "InnerExceptions". This is a read-only collection of "AggregateException":
Wait. This is a collection of "AggregateException"? Yes, because each task could kick off child tasks, and those tasks could have their own exceptions. It's turtles all the way down.
We can get a collection of "real" exceptions by flattening the "AggregateException".
The "Flatten" method will consolidate all of the "InnerExceptions" collections from all of the child collections. The result is that the "InnerExceptions" property is now a single read-only collection of "Exception".
Now we can loop through that and display the messages to the user:
This will display each exception in its own message box -- probably not the best way to display errors to the user. But we're just concentrating on how to get to the exceptions. How we give that information to the user will vary based on our application needs.
Note: "AggregateException" also has a property called "InnerException" (without the "s"). This is an "Exception". We can use this is we *know* that we will only get a single exception back, but unless we know the inner workings of the method that we're calling, we can't be sure of that. This is why we plan for the possibility of multiple exceptions.
Before running our application, let's make things a little more compact. Instead of having an intermediate variable, we'll just "foreach" over our flattened InnerExceptions:
Now when we run the application, we get the following output:
One thing to notice about the output is that the "Fetch Data (with Task)" button is enabled. In our faulted state, we have 2 continuations that run: (1) show the error message and (2) re-enable the button. These continuations both kick off at the same time (when the initial Task is faulted).
The reason for this is that our continuations are both on the same task -- called "peopleTask" in our code. Here's the final method:
When we call "repository.Get", we get a Task back. From there we have 3 continuations. The first only runs if things complete successfully; this will populate the list box. The second only runs if things fail; this will display the errors. The third runs each time and will re-enable the button.
We can take more control over the order that things run if we need to. When we call "ContinueWith" it returns a new task. We can create continuations on the *returned* task which will only run after that task is complete. There's a lot of flexibility available to us depending on what our needs are.
So, we've seen that there's quite a bit of code involved here. We have to worry about checking for a "faulted" state. We have to flatten the AggregateException. These are details that I'd rather not worry about.
Now let's see what we need to do to fix the "await" method.
Exception Handling with "await"
Here's a reminder of the original "await" method:
And here's what we need to do to handle the exception:
All we have to do is add a "catch" block -- just like we would handle an exception in "normal" code. That's it.
This really gives us an appreciation for what "await" gives us. The compiler handles all sorts of things behind the scenes, and all we have to do is treat our asynchronous code (practically) the same as our "normal" code.
Our behavior is a little different from our Task method, though:
Here we're showing the message box with the exception message. But notice that the "Fetch Data (with await)" button is still disabled. That's because we're currently in the "catch" block of our method, and the code in the "finally" block has not run yet.
As soon as we clear the message box, our button is re-enabled:
So this example shows us how easy it is to use "await" in our methods compared to handling the Tasks manually.
Why are we bothering with the manual Tasks at all? The answer is that we can't always use "await" in our code. Sometimes the processing is more complex. Sometimes we need additional features (such as progress reporting). Sometimes we need to have more control over how things run.
I really like to understand what's going on underneath (this is one of the reasons that I look at what's going on when we use the "foreach" loop -- just an easier way to interact with IEnumerable). This often helps me understand why something doesn't work when I try to do something a little out of the ordinary.
Exception Handling in our Console Application
Before wrapping things up, let's take a look at the console application (in the "UsingTask.Tester" project). Here's a reminder of how we left the code:
Due to some restrictions, we can't use "await" here, so we have a continuation set up. But right now, we don't have any continuation options specified.
So if we run the application, we get a runtime error:
This exception is happening in our continuation method. The actual problem is that we are trying to access the "Result" property on a task that has been faulted.
But the code is easy enough to update. This is similar to our WPF code:
We did two main things here. First, we added "OnlyRanToCompletion" on our first continuation. This will ensure that we only try to access the "Result" property if things completed successfully.
Second, we added another continuation that calls the "ShowError" method. This is marked as "OnlyOnFaulted", and the code loops through the flattened exceptions and outputs the message(s) to the console.
And here's the output:
Other Options
This isn't the only way we can deal with exceptions. For example, we can also check the "IsFaulted" property on our Task. With this, we could have a single continuation that uses "IsFaulted", "IsCanceled", and "IsCompleted" to decide what to do (and this is exactly what I do in another example: BackgroundWorker Component compared to .NET Tasks).
As a reminder, all of the code for this article is in the "07-ExceptionBasics" branch on GitHub: https://github.com/jeremybytes/using-task/tree/07-ExceptionBasics.
Wrap Up
So we've seen the basics of dealing with faulted tasks. When we're using Task directly, we want to make sure our continuations have the right "TaskContinuationOption". As an alternative, we can check the status properties of the task before interacting with the results.
More importantly, we see how using "await" makes all of this transparent. We can treat our code very much like "normal" code: just wrap it in a "try/catch" block and handle things from there. The more that I learn about this, the more I appreciate what the compiler does for me. But I also feel empowered: if I run into a situation that "await" can't handle, I can fall back to interacting with Tasks manually.
Happy Coding!
Amazing tutorial Jeremy.
ReplyDeleteNice tutorial.. thank you
ReplyDeleteThis is the best article I've ever seen to explain tasks
ReplyDeletethank you very much
Great article .. Thanks.
ReplyDelete