For the first blog post of 2017, the first after awhile really, I really like to touch on one of the interesting problem that I faced along side with the community of the UWP Community Toolkit over at Github awhile back, on one of the API that I designed for the toolkit – The Dispatcher Helper.
This content touches some deep part of C#, make sure you are familiar with lambda/anonymous function, generics and asynchronous programming before proceeding .
The DispatcherHelper API takes in a function of your choice and execute them on a specified thread dispatcher, usually the UI thread’s dispatcher in most UWP application. The problem arises when the user pass in a synchronous lambda that return a null when the caller is expecting a non-nullable return type. Normally, this would be a compile-time error, but due to the nature of lambda and API with multiple overloads, this became an unexpected runtime error. First, let take a look at the DispatcherHelper API signature:
public static Task<T> AwaitableRunAsync<T>(this CoreDispatcher dispatcher, Func<Task<T>> function, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal)
public static Task<T> AwaitableRunAsync<T>(this CoreDispatcher dispatcher, Func<T> function, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal)
public static Task AwaitableRunAsync(this CoreDispatcher dispatcher, Func<Task> function, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal)
public static Task AwaitableRunAsync(this CoreDispatcher dispatcher, Action function, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal)
The core of the DispatcherHelper API is this function, AwaitableRunAsync, that has 4 overloads for 4 different parameters type : Action, Func, Func<T>, Func<Task<T>> to handle 4 types of function that C# can have. Action is a function that return void, while Func is its asynchronous equivalent. Func<T> is a function that has a return value and T is its type, with Func<Task<T>> is its asynchronous equivalent.So technically you can pass any function into the API to get it executed on the specified dispatcher to get it executed on the UI thread (or any valid thread with a Dispatcher) in an asynchronously manner – one of the most common problems that .NET Developers have to face these days. Now, consider this call:
We have a synchronous lambda which update the NormalTextBlock.Text property and then return null, that is running on the DispatcherHelper.ExecuteOnUIThreadAsync (a wrapper for the AwaitableRunAsync() function) . At first glance, this is pretty normal right?
…Until you see the return type of this call, an int!
And what if I tell you Intellisense and Roslyn don’t complain one bit when you compile this?
Of course, when you try to run this, with the initial version of the API, it will throw a NullReferenceException, as it was reported on issue #608, at a lower level when the code try to await the call to get the return result. Precisely , it broke inside the AwaitableRunAsync overload that has accepts a Func<Task<T>>, at this call :
taskCompletionSource.SetResult(await function().ConfigureAwait(false));
But wait, wasn’t the lambda supposed to be a synchronous function that return an int? Why does it breaks at the await part of the code? You can’t await a synchronous function! And you can’t return null for a function that is returning integer!
From the compiler (Roslyn)’s stand point, this is a completely valid C# syntax at compile time. Why? It is because of the nature of lambda/anonymous function. Let take a step back, look closely again at the lambda that is being passed into the DispatcherHelper:
int returnedFromUIThread = await DispatcherHelper.ExecuteOnUIThreadAsync<int>(() => { NormalTextBlock.Text = "Updated from a random thread!"; return null; });
Via generics, we explicitly declare and expect an int returning from the ExecutionOnUIThreadAsync() call (a wrapper for AwaitableRunAsync()). And if you remember the 4 overloads that we have for the AwaitableRunAsync(), only 2 of them can accept functions that have the ability to return an int: One accepts a Func<T> and the other accepts a Func<Task<T>> (and in this case, T is int). Because of the lambda being synchronous, the Func<T> one should have been used right? Why does the code breaks inside the Func<Task<T>> version of AwaitableRunAsync() then? Is this a compiler bug?
Anonymous Function/Lambda gets its signature at compile time, when the compiler evaluates the call that the anonymous function/lambda is created. The compiler looks at what the lambda is returning and the expecting return type of the call to assign a signature for the lambda. In our example, the way of the compiler thinks when it hits the lambda above is:
– Oh, hey. I have a lambda that is returning null, so the return type must be nullable!
– And how many choices do we have in ExecuteOnUIThreadAsync() again? Only 2?
– Any of them is nullable?
– Oh well, I know for a fact that int is NOT nullable, so I just chose the one that is returning Task<int> because that one is definitely nullable!
…
(crash)
So in other word, our null-return-synchronous lambda was passed inside an overloads that was made to deal with asynchronous function because the compiler has no other choice. When we try to await the call, the function return back null to the await statement , hence the NullReferenceException.
Of course, this is a user-error for returning a null inside an int-returning function, but it is not always clear to the developer that they are doing something that they are not supposed to do. Like I previously pointed out, Intellisense and Roslyn both did not catch this because it is completely legal syntax and logic (in this case at least) and the developer has no way to know that they messed up without good unit testing. And not only our home-grown API is effected by this problem, if you try the same thing with Task.Run(), you will run into the same problem with the same reason.
After a lot of discussion(here, and here), the community has come to an agreement of throwing a much more detailed exception to notify the developer that something has gone terribly wrong, rather than some random NullReferenceException. You can view the detailed here at pull #618 by Lukas Fellechner.
So next time, remember to watch your back if you are returning a null inside a lambda, you might be doing something horribly wrong.
Code on!