RxJS interop incompatibility: A tale as old as time

Simon Stevens
AppLearn Engineering
3 min readJun 28, 2022

The story of an interesting bug that will almost certainly only appear within our unique setup.

Uncaught TypeError: You provided an invalid object where a stream was expected.  You can provide an Observable, Promise, Array, or Iterable.

Our story starts in a place very familiar to us, the Chrome DevTools Console. Here, an error message tells an improbable tale that we are trying to use something that is not an RxJS observable in a place that expects such a thing.

Surely nobody could make such a mistake?

A code review tells me that no, I am not that particular flavour of stupid this week. There must be another reason why RxJS does not recognise it’s own.🤔

Interop observables

A quick Google search yields poor results, but a lonely, mostly ignored comment on Stack Overflow introduces new concepts to my brain: Interop and well-known symbol. One more Google later and I’m diving into a world of interop observables, learning that this well-known symbol is how observables are recognised by their respective libraries.

The question remains though, how can RxJS not recognise it’s well-known symbol from one moment to the next? For that answer, I must go spelunking through code.

The stack from the original error message points me towards subscribeTo as a starting point, and with my shiny new knowledge of symbols I immediately hit upon my first clue

if (!!result && typeof result[Symbol_observable] === 'function') {

https://github.com/ReactiveX/rxjs/blob/6.6.7/src/internal/util/subscribeTo.ts#L15

Aha! Symbol_observable! I know about those! If it’s checking for the presence of this property, then it is presumably defined on the Observable class?

[Symbol_observable]() {
return this;
}

https://github.com/ReactiveX/rxjs/blob/6.6.7/src/internal/Observable.ts#L303

Okay, so what is Symbol_observable?

export const observable = (() => typeof Symbol === 'function' && Symbol.observable || '@@observable')();

https://github.com/ReactiveX/rxjs/blob/6.6.7/src/internal/symbol/observable.ts

So if Symbol.observable doesn’t exist, then it will use its own '@@observable' value instead. Well, that seems like something that could go wonky and end in RxJS not recognising it’s own Observable… except, it either is in the global object or it isn’t. It can’t just exist one minute and not the next. It just can’t!

Except…our architecture uses a micro frontend, with completely separate code bases loaded into the browser at different times. Maybe this is a race condition featuring our new bestie Symbol.observable?

Typing Symbol.observable into the console of our test environment outputted Symbol(Symbol.observable), so it exists. What if it didn’t exist though? In Nicholas Jamieson’s article I found during my initial searches, he mentions that the proposal for introducing Symbol.observable to the ECMAScript standard library had been stalled since 2017, so surely it shouldn’t exist.

Polyfills…? If it exists, it must be being polyfilled. Our micro frontends load independently via script tags, so load at different times. If the frontends are polyfilled differently, and load at different times, then that could explain how the Symbol.observable changes. I can see the cause now, but how to prove it?

I throw a breakpoint into dev tools to stop our micro frontend from loading, leaving just the orchestrator, the source of the observable. I run Symbol.observable in the console again and….. undefined. I allow the micro frontend to continue loading, run Symbol.observable again and... Symbol(Symbol.observable).

Well that proves that then. Symbol.observable, as far as RxJS sees it, changes at run time. Our orchestrator creates an observable when Symbol.observable does not exist, causing the observable to use @@observable instead. By the time that observable has been passed to our micro frontend, Symbol.observable has been polyfilled and RxJS is looking for Symbol.observable, not @@observable. It doesn’t recognise itself, and throws up the following.

Uncaught TypeError: You provided an invalid object where a stream was expected.  You can provide an Observable, Promise, Array, or Iterable.

This is where this particular story ends.

With the above knowledge, we added the symbol-observable polyfill to our orchestrator and the bug was fixed. There was a party, and my colleagues may or may not have commissioned a statue in my honour. However, whether you believe that last part of not my name will live on in the history of AppLearn as the greatest bug hunter who ever lived. 🤣

--

--