A promise is only as good as the code that makes it
When I make a promise to someone, I do my best to keep it. If I’m pretty sure I won’t be able to do something, I don’t make any promises about it. Instead, I say I’ll try to address it eventually. It’s all about managing expectations. In some ways, a promise is like a software interface — a kind of contract between the other person and me.
With asynchronous computations, we make promises in software too. They are similar to the promises you and I make, representing operations that haven’t happened yet but are expected to happen in the future. In JavaScript, for example, there is an explicit Promise1 construct. In .NET, this is done with the System.Threading.Task
class.
Unfortunately, not everyone takes promises seriously — both in real life and in software. Sometimes people promise but don’t deliver, violating the implicit contract of a promise. In software, null references are examples of implicit contract violators. Tony Hoare once called null references his billion dollar mistake.2 While C# doesn’t explicitly prevent it, you should avoid returning null
at all costs when using Task-based APIs. The reason for this is that returning null from such an API can result in a NullReferenceException
, and that can end up masking other production problems. Let’s see how to avoid these kinds of “null promises.”
🔗Null doesn’t deliver on its promise
Returning null from Task-based APIs can be tempting. Even within a code base that’s mostly asynchronous, we might write some synchronous code. Let’s take a look at how we might model the real-world promises our friends make to us in software:
interface IFriend {
Task PromiseMeSomething();
}
The above IFriend
interface should be implemented by my friends. Every time I ask them to promise something, I would call the interface’s PromiseMeSomething
method. Good friends never let you down, but there’s always this one “friend” who just doesn’t deliver:
class SomeoneWhoPromisesButDoesntDeliver : IFriend {
public Task PromiseMeSomething() {
Console.WriteLine("I promise you...");
return null;
}
}
Here we can see that the SomeoneWhoPromisesButDoesntDeliver
class returns null
from the PromiseMeSomething
method. Since the code is all synchronous, that seems fine, at least to the compiler. After all, null
is a valid return type for reference types like Task
. Unfortunately, there are consequences to this implementation for the caller. Let’s see what happens when I ask for a promise from my soon-to-be-former friend:
class Me {
public static async Task WithALittleHelpFromMyFriends() {
var exFriend = new SomeoneWhoPromisesButDoesntDeliver();
await exFriend.PromiseMeSomething();
}
}
When we execute this code, our “friend” causes a NullReferenceException
(that son of a…). Unfortunately, analyzing the stack trace of the thrown NullReferenceException
doesn’t give us much:
at Program#10.<Me>d__1.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at Program#11.<<Initialize>>d__0.MoveNext()
Since the invocation of PromiseMeSomething
is done asynchronously, the compiler generates a state machine to execute it. It’s in that generated code that the NullReferenceException
is thrown. The problem here is that this stack trace doesn’t give us any idea as to which code caused the exception. Was it in the Me
class in SomeoneWhoPromisesButDoesntDeliver
? Or was it some other code that SomeoneWhoPromisesButDoesntDeliver
called? There’s no way to know other than stepping through the code, line by agonizing line. Now imagine how much more frustrating it would be if we were iterating through a collection of IFriend
objects rather than calling just one.
🔗Promise and deliver!
As we just saw, trying to await
a null
object results in a NullReferenceException
. So, even if your code isn’t async
, you should always return a proper Task
object just in case your caller is async
. One way of doing this (in .NET 4.6 or higher) is by returning Task.CompletedTask
— a preallocated and already-completed Task
object that can be awaited.
Now, let’s take a look at an implementation of IFriend
from a good friend who would not let me down, even if implemented synchronously:
class SomeoneWhoPromisesAndDelivers : IFriend {
public Task PromiseMeSomething() {
Console.WriteLine("I promise you...");
return Task.CompletedTask;
}
}
🔗Avoiding null
It’s all well and good to tell our friends not to return null
in their promises, but our code should be safe for any of our callers — meaning we should check for null
before awaiting any task. Modifying our earlier method, we would have this:
public static async Task WithALittleHelpFromMyFriends() {
var exFriend = new SomeoneWhoPromisesButDoesntDeliver();
var promise = exFriend.PromiseMeSomething();
if (promise != null)
await promise;
else
throw new SomeMeaningfulException();
}
But it’s annoying to always have to check for null, so consider using extension methods to clean things up:
public static async Task WithALittleHelpFromMyFriends() {
var exFriend = new SomeoneWhoPromisesButDoesntDeliver();
await exFriend.PromiseMeSomething().ThrowIfNull();
}
The extension methods can then handle the boilerplate code:
public static Task<T> ThrowIfNull<T>(this Task<T> task)
{
if (task != null)
{
return task;
}
throw new Exception(TaskIsNullExceptionMessage);
}
public static Task ThrowIfNull(this Task task)
{
if (task != null)
{
return task;
}
throw new Exception(TaskIsNullExceptionMessage);
}
With these extension methods, if our IFriend
returns null
, we’ll see exactly where it happens.
🔗Summary
If a method requires you to return a Task
and you need to implement the method synchronously, return Task.CompletedTask
instead of null
to avoid a NullReferenceException
happening deep in the call stack of the compiler generated code. You can also use extension methods to protect yourself from code that might return null
instead of a Task
.
If you want to find out more about avoiding pitfalls in asynchronous codebases, check out our webinar series.
🔗Footnotes
About the Author: Daniel is a solution architect at Particular Software and leads the asyncification of NServiceBus and the ecosystem around it. He doesn’t await for good things to happen. He makes them happen.