Skip to main content

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.

Share on Twitter
Don't miss a thing. Sign up today and we'll send you an email when new posts come out.
Thank you for subscribing. We'll be in touch soon.
 
We collect and use this information in accordance with our privacy policy.