Observables can be a pain to debug. Most debuggers aren’t particularly suited for tracing streams of data, and the stack traces you get are unimpressive. Let’s dive a little deeper into the stack traces. Take this piece of code which throws an error after a few integers have passed through the stream.
[Fact]
public void ObservableTest()
{
IObservable<int> observable = Observable.Range(0, 5)
.Select(i => i * 2)
.Do(i =>
{
if (i > 5)
{
throw new Exception("That's an illegally large number");
}
});
observable.Subscribe(
onNext: (i) => Console.WriteLine(i),
onError: (err) =>
{
throw err;
});
}
The humongous Stacktrace is as follows:
Error Message:
System.Exception : That's an illegally large number
Stack Trace:
at MyProject.Test.ObservableTest.<>c.<Foo1>b__0_3(Exception err) in /home/geewee/programming/MyProject.Test/ObservableTest.cs:line 30
at System.Reactive.AnonymousSafeObserver`1.OnError(Exception error) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\AnonymousSafeObserver.cs:line 62
at System.Reactive.Sink`1.ForwardOnError(Exception error) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 61
at System.Reactive.Linq.ObservableImpl.Do`1.OnNext._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Do.cs:line 42
at System.Reactive.Sink`1.ForwardOnNext(TTarget value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 50
at System.Reactive.Linq.ObservableImpl.Select`2.Selector._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Select.cs:line 48
at System.Reactive.Sink`1.ForwardOnNext(TTarget value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 50
at System.Reactive.Linq.ObservableImpl.RangeRecursive.RangeSink.LoopRec(IScheduler scheduler) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Range.cs:line 62
at System.Reactive.Linq.ObservableImpl.RangeRecursive.RangeSink.<>c.<LoopRec>b__6_0(IScheduler innerScheduler, RangeSink this) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Range.cs:line 62
at System.Reactive.Concurrency.ScheduledItem`2.InvokeCore() in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Concurrency\ScheduledItem.cs:line 208
at System.Reactive.Concurrency.CurrentThreadScheduler.Trampoline.Run(SchedulerQueue`1 queue) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Concurrency\CurrentThreadScheduler.cs:line 168
at System.Reactive.Concurrency.CurrentThreadScheduler.Schedule[TState](TState state, TimeSpan dueTime, Func`3 action) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Concurrency\CurrentThreadScheduler.cs:line 118
at System.Reactive.Concurrency.LocalScheduler.Schedule[TState](TState state, Func`3 action) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Concurrency\LocalScheduler.cs:line 32
at System.Reactive.Concurrency.Scheduler.ScheduleAction[TState](IScheduler scheduler, TState state, Action`1 action) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Concurrency\Scheduler.Simple.cs:line 61
at System.Reactive.Producer`2.SubscribeRaw(IObserver`1 observer, Boolean enableSafeguard) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Producer.cs:line 119
at System.Reactive.Producer`2.Subscribe(IObserver`1 observer) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Producer.cs:line 97
at System.ObservableExtensions.Subscribe[T](IObservable`1 source, Action`1 onNext, Action`1 onError) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Observable.Extensions.cs:line 95
at MyProject.Test.ObservableTest.ObservableTest() in /home/geewee/programming/OAI/MyProject.Test/ObservableTest.cs:line 25
Removing all the Rx.Net internal stuff, this is the only information in the stack trace we care about:
Error Message:
System.Exception : That's an illegally large number
Stack Trace:
at MyProject.Test.ObservableTest.<>c.<ObservableTest>b__0_3(Exception err) in /home/geewee/programming/MyProject.Test/ObservableTests.cs:line 30
at MyProject.Test.ObservableTest.ObservableTest() in /home/geewee/programming/OAI/MyProject.Test/ObservableTests.cs:line 25
We get the line number where we subscribed to the observable, and the function inside the chain where the error was thrown. I have no idea what transformations the data has gone through. If the stream has been dynamically constructed I might not even know what it looks like.
These issues aren’t unique to observables. Composing long LINQ-expressions suffer from the same issues. When composing several different functions together, the stack traces very quickly stop becoming meaningful. The below is the complete stack trace the Enumerable/LINQ version of the Observable code.
Error Message:
System.Exception : That's an illegally large number
Stack Trace:
at MyProject.Tests.ObservableTests.<>c.<TestLinq_StackTraces>b__2_1(Int32 i) in /home/geewee/programming/MyProject.Test/ObservableTests.cs:line 55
at System.Linq.Utilities.<>c__DisplayClass2_0`3.<CombineSelectors>b__0(TSource x)
at System.Linq.Enumerable.SelectRangeIterator`1.MoveNext()
at MyProject.Tests.ObservableTests.TestLinq_StackTraces() in /home/geewee/programming/MyProject.Test/ObservableTests.cs:line 60
It’s much cleaner, but it doesn’t give us any more information than the observable version.
The poor debugging and stack-traces are a perpetual thorn in my side when working with Observables. But if the same issues exist with long chains of Enumerable, why is that less of an issue? The short answer — I don’t know.
My best guess is that when using Observables there’s a strong tendency to keep everything as an Observable stream. The longer your streams are the harder debugging with a minuscule stack trace becomes.
It’s Hard To Use Scopes
A very common thing I need to do is attach some sort of context to a request or a series of events.
var id = "myCoolId"
using (LogContext.PushProperty("id", id))
{
// Every log statement in here will have the `id=myCoolId` attached
await DoSomething(id);
}
This is a very common usage patterns for logging, for example in Web Apps if you want to attach a specific GUID to a Request, so you can later correlate all logs for that specific request.
Something like this isn’t possible when using Rx.Net, as it’s threading model doesn’t carry over the ExecutionContext.
This means that we can’t use something like an AsyncLocal or a ThreadLocal to keep context for a specific piece of data or stream. If we want the myCoolId to be attached to all of the logs, we'll need to pass it into every step of our observable chain, and manually pass it to every logging call. I know there's a value in being explicit, but this is a time where choosing Rx.Net locks you out of some pretty handy language features.
It never really took off
Looking at the timelines, it seems like Rx.net in the hype cycled peaked a few years ago. If it ever really took off.
Google Trends for system.reactive compared to the Javascript version of the ReactiveX framework.
Comparing it in google searches to RxJS it seems much less widely used. Looking at the Stack Overflow trends reveals a little more nuanced picture however:
Stack Overflow trends for system.reactive compared to the Javascript and Java versions of the ReactiveX framework.
While still much less popular than RxJS and the Java ReactiveX version, Rx.Net does seem to predate their popularity by quite a few years. Microsoft was out early with the reactive paradigm but never really seemed to manage to make it take off.
Now while your technology choices shouldn’t be about how hyped something is — the popularity is always worth taking into account. Some old technologies are sturdy, battle-tested and well-documented. Then they don’t have to be shiny.
However with some of the pitfalls, particularly around the documentation issues, Rx.Net feels neither robust or shiny to me.
While this is a negative article, I’m not saying Rx.Net or the Reactive paradigm is always bad. Some of the issues are about the tooling and documentation. I mention that most debuggers aren’t suited for debugging streams, but this is a tooling problem and not inherent to the reactive paradigm. E.g. Jetbrains has an excellent Java Stream Debugger.
Observables seems like a very natural fit for some languages and domains, like reacting to user interactions in JavaScript, as the large adoption of RxJs suggests. I’m just saying that it doesn’t feel like a particularly natural fit for C#, and if you have some data that can be modelled as either Enumerables or Observables, I would think twice about using Observables.
Source: Meidum
The Tech Platform
Comments