The problem still exists with the current version of the System.Reactive library (5.0.0). I made some progress identifying its cause. The problem is related with the naughty subscribers that omit the onError
handler. When this handler is omitted, the Rx puts something like this in its place:
void OnError(Exception error) => throw error;
The error is just rethrown synchronously on the thread that invoked the handler! This has the implication that no other subscriber after the naughty one will receive the OnError
notification, because an observable notifies all of its subscribers sequentially and synchronously.
var subject = new Subject<Unit>();
subject.Subscribe(); // Naughty subscriber
subject.OnError(new ApplicationException("@")); // Throws synchronously
(Try it on Fiddle)
Normally this is not a problem, because most notifications coming from built-in Rx operators are invoked asynchronously from ThreadPool
threads. This means that there is no way for the client code to catch the exceptions caused by naughty subscribers. And so the exceptions remain unhandled, and are crashing the process. Which is not an ideal behavior for a commercial application, but it is surely much nicer than a hanging process, waiting for a notification that will never come. I am saying this to make it clear that the problem we are trying to solve here is how to make the application crash consistently. When an error occurs and we have a naughty subscriber, crashing the process is the desired behavior. So let's find a way to make it happen with the Observable.Create
too.
The problem with the native Observable.Create(Func<IObserver<TResult>, Task>)
method is that invoking the asynchronous lambda produces a Task
, and the tasks have embedded error handling features. So an exception inside the lambda doesn't escalate automatically to an unhandled exception that crashes the process. My first attempt to fix the Observable.Create
method was to await
the task, catch the exception, try to propagate it again through the observer.OnError
method, and if this attempt failed then rethrow the exception on the ThreadPool
and crash the process for good. But this idea is flawed because only the first invocation of the observer.OnError
throws. Subsequent invocations are doing nothing, because the built-in observers respect the Rx contract, which disallows multiple OnError
notifications. So the try/catch must target specifically the observer.OnError
invocation, not the whole asynchronous lambda in general. Which means that we need a wrapper of the supplied observer. The implementation below is based on this idea:
public static IObservable<TSource> CreateObservableEx<TSource>(
Func<IObserver<TSource>, CancellationToken, Task> subscribeAsync)
{
return Observable.Create<TSource>(async (observer, cancellationToken) =>
{
var innerObserver = Observer.Create<TSource>(observer.OnNext,
PropagateError, observer.OnCompleted);
try { await subscribeAsync(innerObserver, cancellationToken); }
catch (Exception ex)
{
PropagateError(ex);
}
void PropagateError(Exception error)
{
try { observer.OnError(error); }
catch (Exception ex)
{
ThreadPool.QueueUserWorkItem(_ => ExceptionDispatchInfo.Throw(ex));
}
}
});
}
Usage example:
var xs = CreateObservableEx<Unit>(async (o, ct) =>
{
await Task.Delay(10, ct);
o.OnError(new Exception());
}).Replay().RefCount();
xs.Subscribe(x => Console.WriteLine(x));
xs.Subscribe(x => Console.WriteLine(x), ex => Console.WriteLine(ex.Message));
await xs.DefaultIfEmpty();
(Try it on Fiddle)
Output:
System.Exception (Unhandled)