I have this unit test in C#
[Fact]
public async void DownloadAsync_ErrorDuringProgress_ThrowsException()
{
using var model = SetupModel();
SetDownload();
async Task<DownloadStatus> Act()
{
return await model.DownloadAsync(VideoUrl, DestFile, (s, e) =>
{
e.Download.ProgressUpdated += (s, e) =>
{
throw new Exception("BOOM!");
};
});
}
// await Act();
await Assert.ThrowsAsync<Exception>(Act);
}
If an exception is thrown in the event handler, then the whole method should throw an exception. (The other option is to handle/ignore the exception and return DownloadStatus.Failed, but then I have to put a generic Catch block that triggers analyzers, and the error in user code is hidden when it should be handled in the event).
For another similar event, it's working as expected, but ProgressUpdated is behaving differently. If I call "await Act();", then an exception is thrown... but it's impossible to catch that exception with ThrowsAsync! The callback is somehow in a different context or something.
It's the System.Progress class that is responsible for triggering ProgressUpdated -- which means that the whole callback piece of code is running in a separate context that doesn't handle exceptions properly.
Looking in the documentation of System.Process, I found this which may be the cause?
Any handler provided to the constructor or event handlers registered with the ProgressChanged event are invoked through a SynchronizationContext instance captured when the instance is constructed. If there is no current SynchronizationContext at the time of construction, the callbacks will be invoked on the ThreadPool.
It's also not clear whether this problem would happen at runtime or only during unit testing.
Anyone understands what's happening, and how to deal with a situation like this? I feel like there's something for me to learn here.
Edit: Here's the code where the context switching would occur
private async Task DownloadFileAsync(DownloadTaskFile fileInfo)
{
Status = DownloadStatus.Downloading;
using var cancelToken = new CancellationTokenSource();
try
{
await _youTube.DownloadAsync(
(IStreamInfo)fileInfo.Stream, fileInfo.Destination, new Progress<double>(ProgressHandler), cancelToken.Token).ConfigureAwait(false);
void ProgressHandler(double percent)
{
fileInfo.Downloaded = (long)(fileInfo.Length * percent);
UpdateProgress();
if (IsCancelled)
{
try
{
cancelToken.Cancel();
}
catch (ObjectDisposedException) { } // In case task is already done.
}
}
}
catch (HttpRequestException) { Status = DownloadStatus.Failed; }
catch (TaskCanceledException) { Status = DownloadStatus.Failed; }
}
Where else did I hear about this unhandled errors problem? "async void". This event handler is not async BUT it is being run async.