But we have alternatives. We can consolidate the code into a single continuation and then use properties on the Task itself to determine what to do. And that's what we'll look at today. Along the way, we'll look at the "IsFaulted", "IsCompleted", and "Status" properties of our Task.
All of the articles in this series are collected here: Exploring Task, Await, and Asynchronous Methods. And the code is available on GitHub: https://github.com/jeremybytes/using-task.
The completed code for this article is in the 08-AlternateContinuation branch.
[Update 09/2015: For a video version of this article, look here: IsFaulted, IsCompleted, and Task.Status.]
How We Left the Code
As a reminder, here's how we left our method that directly interacts with Task:
This calls the asynchronous "repository.Get()" method. Then we take the Task that comes back from this method and set up 3 continuations.
Let's break these down.
Continuation #1: Success State
The first continuation has the option for "OnlyOnRanToCompletion". This means that this will only run if the Task completes successfully. In that case, we get the "Result" and use it to populate the list box in our UI.
Continuation #2: Faulted State
The second continuation has the option for "OnlyOnFaulted". This means that this will only run if the Task throws an exception. If that's the case, we display any exceptions in a message box. But we could just as easily rethrow the exceptions on the UI thread, log them, or display them to the user in a different way.
Continuation #3: Any State
The third continuation does not have any continuation options. So it will run regardless of the final state of the Task. We use this continuation to reset our button to the "enabled" state.
A Single Continuation
As mentioned, Task is extremely flexible. That means we can handle this functionality in a bit of a different way. Instead of creating continuations that have "TaskContinuationOptions", we can create a single task and simply use properties on the Task itself to decide which pieces of code we want to run.
A Continuation That Always Runs
We'll take our first Task continuation (the "Success State") and combine the functionality from the other continuations. We will want this continuation to run regardless of the final state, so we'll start by removing the continuation option parameter:
We've also removed the cancellation token parameter. We no longer need it since we have an overload of "ContinueWith" that has just "Action" and "TaskScheduler" as parameters.
Rolling in the Other Continuation Code
Next, we'll copy the body of our second continuation (the "Exception State") above our current code.
Then we'll copy in the body of our last continuation (the "Any State") at the bottom.
Checking the IsFaulted Property
Now we have all of our code in a single continuation. But we have a bit of a problem. What happens if we run the application, and the Task does *not* throw an exception?
We get a runtime error. Since the Task is not faulted, the "Exception" property is null. So we get a "NullReferenceException" when we try to access it.
Fortunately, Task has a property called "IsFaulted" that will tell us whether our Task has exceptions. We can use this to wrap our error handling code.
When "IsFaulted" is true, we know that the "Exception" property is populated, and we can safely access it and deal with our faulted state.
But what if our Task *does* throw an exception?
Now we have a problem when we try to access the "Result" property. This shouldn't be a surprise. This is exactly the same behavior we saw when we were looking at Basic Exception Handling before we added the continuation options.
So, we only want this code to run if the Task is *not* faulted. That's easy enough with an "else" statement.
What About Cancellation?
There is one other possibility that we need to consider here: cancellation. Now, we haven't covered cancellation yet (we'll spend plenty of time on that in a future article), but we need to be aware that the Task may be canceled.
If the Task is in a canceled state, then the "Result" property is not valid, and we will get an exception if we try to access it. So simply having the "else" statement here isn't quite good enough.
Now in addition to "IsFaulted", we have some other properties on our Task, including "IsCanceled" and "IsCompleted".
Now we might be tempted to use the "IsCompleted" property the same way that we used "IsFaulted". That would look something like this:
But what we'll find is this does not work. To understand why, let's take a look at the Task properties in Help:
Notice that all three properties state that the Task has completed. This means that if we get an exception in the Task, both "IsFaulted" and "IsCompleted" will both be true.
We cannot use "IsCompleted" to mean "completed successfully". "IsCompleted" really just means "is no longer running".TaskStatus to the Rescue
Fortunately for us, there is another property that we can use for this purpose. Task has a "Status" property which is of type "TaskStatus". This is an enum with the following values (from Help):
We can use this property to check the "Canceled" or "Faulted" state of the Task. We can also use it to see if the Task is currently running or waiting to run. But the one that we care about here is "RanToCompletion".
"RanToCompletion" is the equivalent of the continuation option that we used initially: "OnlyOnRanToCompletion". So let's use this in our code:
Now our code behaves as expected. If the Task throws an exception, only the first "if" statement will be true, and our error handling code will run. If the task does not throw an exception and it completes successfully, then only the second "if" statement will be true, and our code that loads the list box will run.
And regardless of whether the Task completes successfully or is faulted, the button will be re-enabled. (Just to be safe, we could wrap this in a try/finally block like we did with the "await" method, but we won't worry about that for now).
Note: Instead of using "IsFaulted", we could check for "TaskStatus.Faulted". And since "TaskStatus" is an enum, it would be very easy to set up a "switch" statement to check the different states. Ultimately how we implement the code will depend on the level of complexity and what makes the code easiest to read.
Success State Output
Let's run the application. To start with, we'll use the "success" state. For this, we'll check the "Get" method in the "PersonRepository" class (part of the "UsingTask.Library" project) and make sure that the exception is *not* being thrown.
When we click the button, we see the button is initially disabled:
Then after our Task completes, the list box is populated and the button is re-enabled:
Looks good so far.
Exception State Output
Now let's update our "Get" method so that is *does* throw an exception.
When we click the button, it is initially disabled:
Then the message box shows the exception:
Notice that the button is still disabled. This is because the modal dialog stops the rest of the code in the continuation from processing.
As soon as we clear the dialog, the button is re-enabled:
So we have *almost* the same behavior that we had before. When we had the separate continuations, the button was re-enabled at the same time as the error message was displayed. That's because both continuations kicked off at the same time.
But since we have a single continuation now, the button is not re-enabled until after we clear the error message. This matches the behavior that we saw in our "await" method in the prior article.
Let's take a look at our final method.
Instead of having 3 continuations, we have a single continuation that runs regardless of how the Task completed (success, error, or cancellation). We use properties on the Task itself to handle the different states.
As a reminder, the completed code for this article is in the 08-AlternateContinuation branch.
Which Approach Should We Use?
Ultimately, whether we choose to have a single continuation or multiple continuations is up to us. As we've seen, Tasks are extremely flexible. If we have a fairly simple set of completion code (as we have here), then we may want to keep things in a single continuation. Personally, I think this code is a bit easier to follow than the code that we started with.
But if our needs are more complex, we may want to keep the continuations separate. For example, we could have continuations that kick off additional child tasks. Or we could have continuations that have their own continuations. In those situations, we may want to keep things separate so it is easier to follow the flow of each operation.
Choices are good, but sometimes they can feel a bit overwhelming. My approach is to generally start out as simple as possible -- keeping things easy to read. As complexity builds, then I split things out into different units -- keeping things fairly small and (hopefully) easy to follow.
When we have multiple options available to us, we can pick the one that best fits our needs and the needs of our application.