One of the unwritten laws in procedural programming is that any function you call will, at one point, end. In the presence of exceptions, this does not mean that the function will return gracefully, but it will end non-the-less.
When this does not happen, something very strange is afoot. For example, C’s exit()
function ends a program right here and now. But this was a WPF application in C#, only using official libraries. Surely there was nothing like that there. And all I was doing was trying to dispose of my SignalR connection on on program shutdown.
I had registered a delegate for “Exit” in my App.xaml’s <Application>
. The SignalR client only implements IAsyncDisposable
, so I made that shutdown function asynchronous using the async
keyword. I await
ed the client’s DisposeAsync
and the program just stopped right there, not getting to any code I wanted to dispose of after that. No exception thrown either. Very weird.
Trying to step into the function with a debugger, I learned that the program exited when the SignalR client’s DisposeAsync was await
ing something itself. Just exited normally with exit code 0.
At that point, it became painfully obvious what was happening. async
functions do not behave as predictably as normal function. Whenever they are awaiting something, their “tail” is in fact posted to a dispatcher, which resumes the function at that point when the awaited Task
is completed. But since I was already exiting my application, the Dispatcher
was no longer executing newcomers like the remainder of my shutdown sequence.
To fix this, I reversed the order: when a user triggers an application exit, I first clean up my client and then trigger application exit.