Skip to content

[Bug]: List Switch drops completion and throws errors instead of delivering them#1138

Description

@dwcullop

Describe the bug 馃悶

The list Switch operator does not deliver terminal events. Completion is dropped, and errors escape as thrown exceptions rather than reaching the observer's OnError.

Switch relays changes to its observer from a private SourceList<T>:

var populator = Observable.Switch(
    _sources.Do(_ => { lock (locker) { destination.Clear(); } }))
    .Synchronize(locker).PopulateInto(destination);

var publisher = destination.Connect().SubscribeSafe(observer);

That list only ends when it is disposed, so it cannot terminate the output on its own, and nothing carries the terminal event of the source across to the observer. There is no error channel at all: PopulateInto has no error handler, so an OnError from either the sources sequence or the current inner sequence propagates back up the call stack instead.

The error behaviour is the more serious half. Throwing out of OnError breaks the Rx contract, and the exception surfaces at whoever called OnError, which may be an unrelated operator or user code with no way to associate it with this subscription.

Step to reproduce

var log = "";
var observer = Observer.Create<IChangeSet<int>>(
    _ => log += "N", _ => log += "E", () => log += "C");

// Completion is dropped
var inner = new SourceList<int>();
inner.Add(1);
var outer = new BehaviorSubject<IObservable<IChangeSet<int>>>(inner.Connect());
using var sub = outer.Switch().SubscribeSafe(observer);
outer.OnCompleted();
inner.Dispose();
// log is "N". Observable.Switch would give "NC".

// Errors are thrown rather than delivered
var innerSubject = new Subject<IChangeSet<int>>();
var outer2 = new BehaviorSubject<IObservable<IChangeSet<int>>>(innerSubject);
using var sub2 = outer2.Switch().SubscribeSafe(observer);
innerSubject.OnError(new InvalidOperationException("boom"));
// throws InvalidOperationException out of the OnError call.
// Observable.Switch would deliver it to observer.OnError.

Both the inner and outer error cases behave this way.

Expected behavior

Identical semantics to Observable.Switch:

  • Completes once the sources sequence and the inner sequence it last produced have both completed.
  • Delivers errors from either the sources sequence or the current inner sequence to OnError.
  • A superseded inner sequence completing does not terminate the result.

DynamicData Version

main

Additional information 鈩癸笍

Found while fixing the same class of bug in the cache Switch (#1136, #1137). The cache version at least had an error channel, so it only lost completion. The list version has neither.

Raised separately from #1137 to keep that change scoped to the cache.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions