DEV Community

Ayush Maurya
Ayush Maurya

Posted on

Why JavaScript Never Adopted RxJS — It Solved a Problem JavaScript Never Had

A junior developer once asked me:

“RxJS has been around for years, and so many developers use it.

Why hasn’t JavaScript just added something like it natively?”

To which I answered:

Reactivity is mainly a frontend problem. It’s about dealing with one user’s continuously changing state — events, inputs, UI updates. The backend, in contrast, deals in stateless operations. Each request stands alone. The state lives in databases, not in-memory streams. You don’t “observe” backend data changes directly — you react through messages, queues, or databases. Kafka or Redis Streams handle that domain, not RxJS.

That was my quick answer.

But the question was too interesting to leave there — so I dug deeper.


RxJS Has Proven Itself

RxJS has powered Angular’s reactivity model for over a decade.

It’s elegant in handling asynchronous data, widely respected, and deeply integrated in the frontend ecosystem.

Yet despite its longevity and popularity, Observables never became part of JavaScript itself.

The reason is simpler — and deeper — than most realize:

RxJS solved a problem JavaScript itself never really had.


What RxJS Actually Solves

RxJS is built on the concept of Observables — objects that represent streams of asynchronous data that can emit multiple values over time.

  • A Promise resolves once.
  • An Observable keeps emitting.

That makes it perfect for situations like:

  • Listening to continuous user input
  • Handling WebSocket data
  • Managing UI state that updates over time

It’s powerful and expressive — you can map, filter, debounce, combine, and merge asynchronous streams just like you manipulate arrays.

So, if it’s that powerful... why didn’t JavaScript itself embrace it?


JavaScript’s Evolution Is Conservative by Design

JavaScript’s core design philosophy is universality and minimalism.

The TC39 committee — the group that decides what gets added to the language — only standardizes features that:

  1. Apply across all JavaScript environments (browser, server, embedded)
  2. Don’t introduce unnecessary conceptual weight or complexity

RxJS breaks both of those principles.

Reactivity is mainly a frontend concern — about one user’s continuously changing state.

The backend’s problems are different: stateless operations, distributed events, and persistence handled by external systems.


The Observable Proposal That Never Landed

This wasn’t just theoretical — JavaScript almost adopted Observables.

In 2017, a TC39 proposal sought to introduce a minimal Observable type. It reached Stage 1, attracted attention… then quietly stalled.

Why It Failed

  • RxJS already provided a complete, mature ecosystem.
  • Observables are inherently complex — cold vs. hot, cancellation, multicasting, operator design, interop with Promises, etc.
  • Frameworks had already diverged:
    • Angular leaned on RxJS
    • React favored hooks and state
    • Vue built its own reactivity model

The committee couldn’t justify making one paradigm official when the community was already solving the same problem in multiple, incompatible ways.

So JavaScript took the conservative route:

standardize the smallest useful abstraction — Promises — and let the ecosystem handle the rest.


Promises Were the Problem JavaScript Did Have

Promises fixed a universal issue: callback hell.

Before Promises, async code was deeply nested, hard to reason about, and full of error-handling traps. Every JS developer — frontend or backend — needed a cleaner model for single-result async work. Then async/await came, making async code look synchronous — elegant and readable. That was the problem JavaScript actually needed to solve: a single-value, eventual result.

Observables, on the other hand, were about ongoing reactivity — a smaller, domain-specific need mostly confined to the frontend world.

So Promises became part of the language; Observables remained a library concern.


Signals: The Simpler Next Step in Reactivity

But reactivity didn’t disappear — it evolved.

Signals are the next iteration: smaller, simpler, and more predictable.

Signals aren’t streams of events like Observables. They’re reactive state containers that automatically track dependencies and update when values change. You don’t subscribe to them; you just declare relationships.

Example

const count = signal(0);
const doubled = computed(() => count() * 2);

count.set(5);
console.log(doubled()); // 10 — automatically updated
Enter fullscreen mode Exit fullscreen mode

No subscription. No operators. No teardown.
Just reactive state, tracked automatically.

Signals shift reactivity from streams of events to graphs of state — simpler, more deterministic, and easier to optimize.

Thanks for Reading ❤️

If this article helped you, share it and give it like.
It’ll hardly take a minute, but it really motivates me to write more deep-dive articles like this.

I don’t just write “how-tos” — I write about why things are designed the way they are,so you can master them inside-out.

Further Reading

Top comments (7)

Collapse
 
dariomannu profile image
Dario Mannu

Reactivity is mainly a frontend problem.

Actually, it might not. What if you could formulate any CLI or back-end problems in terms of streams that react to events?

  • CLI: stdin |> pipeline |> stdout
  • API: request |> pipeline |> response

where pipeline is an ordinary RxJS stream (Subject/BehaviorSubject) with operators. Everything else is a stream, too.
A DB query? It's a query |> DB endpoint |> result stream. You can switchMap it from any other stream.
A DB change? You do "observe" it indeed. Many modern DB enable subscriptions, so what you get is a stream of changes/updates.
A Kafka/Redis stream? It's already a stream, so you can wrap it in an Observable format without feeling it's cheating.

"Everything is a Stream": this is a hypothesis and mental model that lead us to start working on an actual new paradigm, Stream-Oriented Programming, where all application logic is defined as streams, and where streams, templates and components are the architectural building blocks.

So, reactivity may just be the surface of a larger model that could cover all application development problems. At the end of the day, Observables have been modelled by mathematicians after general-purpose patterns and algebraic structures of functional programming.

Now comes the big question: WHY?
Why should everything be a stream? Because it becomes much simpler to work with, despite these unproven and overhyped Signals claims. If a is promise, b is a number, c is an observable, d is a signal, e is a maybe, writing custom code around these and making sure it works is a nightmare.
If everything is a stream, you don't care anymore, just pipe it up and you know it will work.

Types? Every stream has an input type and an output type. All you need to match is the output of one with the input of the next one. Sync, async, doesn't matter.

Finally, a little correction. TC39 has been kind of bypassed in the standardisation process. Google and W3C (WICG) have created a web platform proposal for native Observables, which are available in Chrome already. Even Node.js has shown interest in getting those in...

Collapse
 
ayush_maurya_ profile image
Ayush Maurya

Hi, reactivity in the frontend is completely different from what we deal with in the backend. Reactivity matters most on the UI because it’s the only place where state changes every millisecond and must update instantly. The backend also has events, but those aren’t the same kind of reactivity problem.

Backend systems do produce streams Kafka, Redis Streams, CDC feeds, etc but those are distributed event logs, not fine-grained reactive state graphs like rxjs solves. They’re built for durability replay partitioning and throughput, not live UI-style updates.

From my POV, one of the biggest issues devs face with rxjsis memory leaks. On the frontend, a leak is usually local to one tab, the moment the user closes it, everything resets. On the backend, that same kind of leak would accumulate continuously and take the system down within hours. That’s why backend systems avoid abstractions that make resource lifecycles harder to control.

Collapse
 
dariomannu profile image
Dario Mannu

I've been looking for similarities between front-end UI, CLI apps and server responses, so they all ended up as subsets of the same mental model, but if you prefer to focus on their differences, that's totally fine, just a different way to look at things.

However, we need to do RxJS some justice because it doesn't leak memory.
What you are describing is what happens with Angular that tried to support two such incompatible paradigms as imperative and functional, leaving devs to fill in the gaps (eg. subscriptions), which is where the leaks happen. So, if you want to build great things with RxJS, maybe it's just Angular not being the best choice...

Thread Thread
 
ayush_maurya_ profile image
Ayush Maurya

I understand what you're saying, and I see the appeal of finding similarities between frontend and backend patterns. However, I think unification comes at a cost. When everything is modeled as a stream, we lose the semantic intent that different abstractions communicate.
a Promise tells you it resolves once, an Observable signals it may emit multiple values, a signal represents reactive state. Each carries explicit meaning about behavior.
Flattening everything into streams makes composition uniform, but it makes intent implicit you have to dig into the implementation to understand what's actually happening, rather than the type signature telling you upfront.

Thread Thread
 
dariomannu profile image
Dario Mannu

then you can choose whether that's a problem or an opportunity.

A promise that resolves at most once is a limitation.
An observable that emits just once and then completes gives you an equivalent behavior.

If your code has to check every time whether it's dealing with a promise or an observable and selectively call .then or .pipe/.subscribe everywhere, that's more of a problem than an opportunity.

If you only use an observable, whether you emit 0, 1, or N times, you can keep using the same operators. They're safe, convenient, always the same and scale seamlessly from 0 to N, which you can't tell about promises or other constructs.

Thread Thread
 
ayush_maurya_ profile image
Ayush Maurya

I see you have built rimmel.js and you have thought deeply about the stream oriented programming. you are right Observables can handle the 0-N cases uniformly, but that comes with a cost... subscription management, memory for observer list, cold/hot semantics and multicasting complexity. for truly single emission scenarios, don't you think this will be extra overhead.

this is the same thing as Javascript and typescript debate. Javascript is dynamically typed - you can put any value anywhere, and it's maximally flexible. But we adopted TypeScript specifically to constrain that flexibility, to make types explicit so programs are more predictable and errors are caught early.

When you say promise resolving only once is a limitation i see it as a constraint. making everything as an observable will surely gives you the flexibility, but you'll loose the ability to reason about behavior from signature.

the community chose TypeScript's constraints over JavaScript's flexibility, because predictability and explicit intent matter more than uniformity in large-scale systems.

I'm genuinely curious how you handle resource lifecycle at scale in a pure-stream architecture.

Thread Thread
 
dariomannu profile image
Dario Mannu • Edited

An Observable-Observer pair is nothing intrinsically more expensive than a Promise/Handler pair. Both an Observable and a Promise have a handle to the next function to call, it's just their behaviour which is a bit different here an there.

Of course you may have lighter or heavier Promise implementations with more or less features, better or worse optimisations, A+ or not A+ compliance, Bluebird vs jQuery.when, etc.
With Observables you have the same, whether it's RxJS, WICG Observables, Most.js or others.

However, if you ask your favourite LLM to create a basic promise and observable implementation you'll see how similar they both are in terms of resources, performance, structure, subscription management (Promises can also have multiple .then(callbacks) attached, for instance).

With regard to types: a Promise<string> only means you'll get 0 or 1 strings emitted. An Observable<string> is the same, you'll just get 0..N strings emitted.

Then the important typing aspect of a Stream is that you define both an input and an output type. You work with a Stream<I, O>, so you're fully typed, end-to-end, by always connecting the right output type of one stream with the right input type of another when connecting/composing.

In stream-oriented programming even a dialog box becomes a stream: one that takes a value as a trigger, displays a UI and re-emits whatever the user selects, if anything.

Resource lifecycle is a bit of a bigger topic, then...