Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Observable API Proposal (github.com/domfarolino)
100 points by tosh on July 28, 2023 | hide | past | favorite | 55 comments


Controversial but brave take: I think adding this to JavaScript is a bad idea. It’s not that observables are inherently bad. It’s just that they produce some of the least intuitive code imaginable. I’ve never seen a codebase with observables where I didn’t question the engineering team’s technical motivations. The three horsemen of unmaintainable JavaScript have always been generators, RxJS, and Redux.

I can’t quite find an accurate metaphor to describe my experience with these data design patterns, but the first that comes to mind is “Hollywood accounting.” It’s always the same hat trick. Take a straightforward task of single directional data flow and subdivide it up into a Haskellian map/reduce game of musical chairs.

Don’t get me wrong, I understand the importance of having observability in data streams. But we already have them via the ReadableStream and TransformStream APIs. Combined with native proxies and we’re just about covered on the use-cases described in the examples section.

I’m also suspect of the lack of insight in this explainer on why the two previous proposals were rejected. We need more concrete evidence of why an additional API should be the answer to the question of whether there are too many competing observable frameworks. This isn’t a jQuery or Bluebird Promises scenario where the observerable paradigm is so entrenched in contemporary engineering, or even that a sizable amount of software development would require a third-party library to fill in the gap.

JavaScript has many missing features. This is not one of them.


As someone who has been using rx since 2014 (with a heavy heart) I must agree with you here. 9 out of 10 times there’s a simple, more boring way of solving the problem. I want boring in my life. Boring is good.

The idea that reading code is harder than writing it can take an extreme form with this style of coding imho.

(My other issue is that for me FRP style code, esp. with rx, is just so much fun to write.)


> It’s just that they produce some of the least intuitive code imaginable. The three horsemen of JavaScript have always been generators, RxJS, and Redux.

It took the author of RxJava months to understand the concepts. And he had the author of Rx.Net to explain them to him [1]

It's also strange is that what we realy want is Excel-like reativity. But instead we're always getting Rx-like reactivity.

[1] From his book: https://pbs.twimg.com/media/C0M-U1DXcAADTS3?format=jpg&name=...


A time ago I had a project where we had an intense tree-like data structure where at any given level there may be a finite async-stream. Basically recursively needed to await streams until they finished.

I was using for RxDB based on RxJS, so after a couple nights of banging my head I went into the RxJS chat and asked for help. One of the maintainers of the library who worked at MS said it was a perfect problem and I think I nerd-baited him into helping out...

... we ended up spending like a week on it going back and forth, and neither of us could figure it out. The code we ended up with was frightening.


I'm guessing Jafar is Jafar Husain, the original evangelist of Observables in JavaScript?

https://www.youtube.com/watch?v=lil4YCCXRYc


I don't think that the problem is that JavaScript may or may not have this feature, it's not even that the language is large and all-encompassing; it's that mixing and matching features is highly appealing and very rarely works out well in practice.

JavaScript is not really a single language built around a single specific style or set of needs any more. In this day and age it's an amalgamation of different techniques and styles. You've got classic inheritance and composition for your OOP crowd; you've got your map and reduce for your functional crowd; you've even got your observables for your reactive crowd.

This is all well and good, but I've found that it's hard to write some practical code that blends styles. At some point it's tempting to start adding types, generics and dependency management into a functional project, but it's my experience that this blending ends up getting in the way of itself. Similar story for wanting to do things like having a service that listens to queues in an async way with sync rest APIs. It seems like having a common set of middleware would make it easy to support both layers; however this is easier said than done. These things sort of work but it feels deeply unsatisfying, requiring constant switching between observable and async/await styles with error handling being a constant concern.


I tend to agree. Observables are incredibly powerful, so they seem like they _might_ be a good fit for some use cases where you're dealing with streams of events. And even then, you have to be really determined to not make a mess. For day-to-day event handling, they usually feel like overkill and become really difficult to understand. Like, I don't _want_ to use map, filter, etc to handle non-iterable stuff. It feels clever and cool on the surface but weird and brain-knot-inducing once you look more deeply at it.


The way they show off observables here is solves a lot of problems, but despite offering up Svelte as an example of observables, it doesn't seem to aim to solve the problem that Svelte stores do.

What I really want is a trivial way to say "fire this callback whenever this variable changes."

Mobx actually enables this w/o too much work, and Svelte stores are a really nice syntaxical wrapper around the concept.

Heck just give me what C# had back in 2002 and I'd be happy.


> fire this callback whenever this variable changes

Ouch, welcome to callback hell :(

Futures would be slightly better. The least painful approach to things like that. to my mind, is an explicit event loop, and posting events on a change.


Been there, helped design an entire embedded runtime based[1] around it, didn't mind it.

FWIW Eventing systems are not the same as callback hell. "Tell me when this variable changes" is the heart of a lot of really well engineered systems, including high performance hardware IO. (Fill this buffer w/o bothering the CPU, when the buffer is full, call this function to process its contents).

Eventing systems are nice.

> The least painful approach to things like that. to my mind, is an explicit event loop, and posting events on a change.

That is how Windows works under the covers.

Events are just syntactic sugar on top if that, in replacement of a giant switch/case statement.

[1]https://meanderingthoughts.hashnode.dev/cooperative-multitas...


My experience has been a total opposite. I've been able to deliver quite complex features in a couple of lines of vert readable code thanks Observables. Of course, you can write terrible code with it, but the same goes for every technology.

I think that this presentation from Netflix engineering team is still the best demonstration of how productive you can be wit RxJS: https://youtu.be/FAZJsxcykPs


Maybe it's because I work on embedded system where I only have to worry about a single process, but I find observables really ergonomic.

"If/else" are a core construct in programming languages. Observables add "when", which I think is just as essential. Whenever someone describes an autonomous system, they will use "when X, do Y". So it makes sense to me that code follows that.

A lot of the time I just want to write a piece of code that and plug it into the system without having to worry about coupling or its effect on other parts of the code. Most of the time I don't have clear requirements, and need to stay flexible.

For example, new requirement to turn off the LCD after X minutes of inactivity, and turn it back on when the user presses a button? I just create a new component (instanced based on a configuration flag), plug it into the event bus reacting to ButtonPress events, and call it a day, without having to worry about something else breaking, often without even having to read existing code (except how to get notified of the event).

Even when modifying existing code, it's easier to replace an event, easy to find which components are depending on that event, etc.


I dont think this should be considered controversial. This sounds like a well balanced opinion, and is for sure one I share.

JavaScript has many missing features. This is not one of them.


> It’s just that they produce some of the least intuitive code imaginable.

I agree because I have seen this a lot.

> JavaScript has many missing features. This is not one of them.

While I do agree that abusing Observable might leads to messy code, it's very valuable in highly interactive apps. It provides proper abstraction/algebra which letting you tackle problems like tripple click, which might be extremely tedious to solve otherwise.

And interactivity is one of the natrual thing modern browser should empower developer to achieve (at least for non-die-hard no-JS person).

> But we already have them via the ReadableStream and TransformStream APIs.

I do appreciate that you appreciate simplicity and this sentiment in general. But I feel similar sentiments that led JavaScript to stagnent for a long time (ES6 is just ES4 but more than a decade later).

People like Douglas Crockford found the parity in JavaScript and Lisp, and summarized the beauty in his works. While his book is one of my favorite programming book, the sentiment (JavaScript don't need features because of closure, Lisp-like and all that) was so popular at time, which probably contributes to the stagnant.

(Microsoft and friends was probably happy about this that the web wasn't taking over so fast and they can shit everybody with IE6 for years, and then the mobile and their walled gardens were taking over. In other word, the web had even greater potential in between IE6 and mobile era)

People could really try re-implement their React apps without modern tooling to feel the pain: No ES6 module and abuse closure then cat the all files into one giant ball, only to mess up the order and dependencies. Or without reactivity, updating states everywhere which leads to confusing bugs that making apps out of sync., etc


> The three horsemen of unmaintainable JavaScript have always been generators, RxJS, and Redux.

Redux produces the easiest to follow code when following best practices. It’s boring and works extremely well for its purpose.

Generators may get a bad wrap but async/await are just generators with a different syntax — at least in js land. Would you argue the same for async/await?


I’ve seen this comparison to async/await a few times and I’m not sure how accurate it actually is. It’s true that async functions can be polyfilled via generators, but from what I understand that’s more of a conceptual transpilation by tools like Babel, rather than a runtime implementation.

IMO the `await` keyword is clever abstraction that can make asynchronous code easier to understand in a synchronous way. And unlike the collection-oriented ceremony of generators, calling await on an expression doesn’t imply anything more than the presence of a `then` method on an object.

Anything with a `then` method can be awaited, and so native Promises aren’t actually all the special or sacred. Invoking await on an expression involves no extra machinery, only that a Promise-like object is returned.

And the `async` keyword is even better. It’s completely optional! This keyword only has two effects: allowing the `await` keyword in a function scope, and implicitly returning a promise.

There’s a secret pleasure in all this too. A series of awaited Promises can be wrapped into a single async function. And there’s no need for multiple error checking steps either. All async functions return a promise, so we can wrap the function single try/catch block, or even daisy-chain another promise on the `catch` method. All the pieces fit into each other, and most of them are optional.

Meanwhile in generator land, we’ve got cognitive overload in spades. Yield can only be called from inside a generator, yes that little asterisk next to the function name you might at first glance mistake for an errant multiplication symbol. Sorry, there’s these things called Symbols too. Any object can be made yieldable…or was it iterable…by placing the special symbol on an object class as an interpolated method which returns an iterable. I’m not making this up.

Imagine teaching this to a junior developer who’s spent over a year programming asynchronous code and never once needing a generator. The tide-level for problems solved with generators is always waist-deep, always introduced as a secondary step to achieving another goal.

Generators are almost always hints of a design-flaw introduced much earlier in development, usually by an engineer with enough experience to write code, but not nearly enough to understand how it works.


I agree with your point. On

> Anything with a `then` method can be awaited, and so native Promises aren’t actually all the special or sacred. Invoking await on an expression involves no extra machinery, only that a Promise-like object is returned.

note that _anything_ can be awaited in JS.

``` const x = await null; console.log(x) ```


Huh wow! I did not know that! I think TypeScript always kept this hidden from me, but it makes sense. Another perk of `await` is that it collapses all promises into their final value, regardless of how many nested pending promises are expressed:

  const foo = await Promise.resolve(Promise.resolve("abc"))

  console.log(foo.length) // 3


It's not `await` which is doing that; it's the Promises themselves. For example:

   Promise.resolve(Promise.resolve("abc")).then(console.log)
This will print `"abc"`, not `Promise.resolve("abc")`.


What’s wrong with generators?


I get asked this question a lot — I help maintain redux-saga.

I recently gave a talk where this question came up: https://youtu.be/uRbqLGj_6mI?t=2166

I’ve also imposed generators on a lot of colleagues.

I think they are mostly misunderstood and potentially in the realm of being “magical” — at least on the surface.

The reality is they are syntactic sugar on top of functions with recursive switch statements where each case is a yield point.

They aren’t that hard to grok — the syntax is mind bending. Having code that looks synchronous but behaves asynchronous is “magical.”

I think it does come down to that because everyone loves async/await even tho that’s just sugar on generators where each yield must be a promise.


Personally, I love redux-saga. It took me a little while to wrap my head around it (but 1/10th the amount of time as RxJS), but it is _really_ nice to be able to write complex async logic in synchronous-looking code that's also readable. (At least it was readable for me and the people on my team.) Whenever I had some weird async coordination problem that I needed to solve, I was _always_ able to solve it with redux-saga. I wish more people would use it.


I feel the same way which is why I decided to help maintain the project. Async flow control is very tricky even in js–land. Having watchers live inside of a while-loop is a powerful construct that lends itself to interest flow control patterns.

I'm also in the process of rebuilding redux-saga but without the redux part: https://github.com/neurosnap/starfx

It's still in alpha stage, but it is very reminiscent of redux-saga.


And then there's async generators...


Nothing other than they're one of the more esoteric and misunderstood features of the language.


It’s a great question, and for the folks who knows the answer of why generators misunderstood, they surely discovered this feature in the most unpleasant circumstances.

Generators were introduced during JavaScript’s awkward adolescence, where the cutting edge of ES6 was only possible through Babel (which was then called 6to5). They’re fantastic for turning array like classes into iterable collections. But that’s not what makes them bad.

For a time you had a perfect storm of JavaScript Hell. Promises were polyfilled and rewritten as generators to take advantage of the `yield` keyword. You’ll most likely find out about this trick at the least opportune time: while debugging an async redux action. Now every single stack trace is a high-stakes game of “detangle the Christmas lights!

This was truly a horrible time to be a front end developer. Not only did you have to transpile your code, you had to debug it through a source map wrung through Webpack’s module system. And while you’re picking through this mud ball of a web app, there’s a Super Senior Software Engineer giving a talk on the virtues of observable data management!

I’ve always suspected that these galaxy brain engineers are more like window washers on an ivory tower. And as for the rest of us, we’re just the code gnomes who are shrug when given triangular bricks until told how round bricks are actually better.

I know this is all hyperbole but the emotion is real. For every productive hour of work gained through a new and necessary features, engineers have lost 10-fold on the lesser well-thought out JavaScript. I’m not so sure that native observables will be any different.


Is this an issue with observables or an issue with RxJS allowing you to shoot yourself in the foot?

A saner API (as shown in the proposal) has some obvious benefits in handling certain use cases without necessarily devolving into the crazy streams one sees with RxJS.


I've never understood why redux gets implemented so badly... I feel that the folks who build wacky redux implementations would still write spaghetti managing state without redux.


The biggest issue is that for the first few years there was no single standardized way to write Redux code. Everyone came up with their own patterns, and wrote their own helper libraries. It also required a lot of "boilerplate" code for things like defining action type strings, action creator functions, and hand-written immutable updates.

That's why we published our official Redux Toolkit package in 2019. It standardizes Redux usage, and includes methods that build in our recommended approaches for things like store setup, writing reducers, simpler immutable updates with Immer, and even a complete server data fetching and caching solution called RTK Query:

- https://redux.js.org/introduction/why-rtk-is-redux-today

- https://redux.js.org/tutorials/essentials/part-2-app-structu...

We routinely get highly positive feedback from folks who hated writing old-style Redux, but love using RTK.

Related, we also have a "Style Guide" docs page that provides guidance on what approaches we recommend using when writing Redux code:

- https://redux.js.org/style-guide/


Having used RTK and RTKQ, I cannot believe they come from the same authors. RTK is mostly serviceable, though for most projects, not using Redux is better. I’ve never seen a case when RTKQ was an improvement over fetch().

Obviously, deleting Redux from the internet to prevent incompetent engineering leads / managers from forcing it on people would be preferable.


If you can't understand the benefits of using RTK Query over fetch I'm not sure you're credible.

They're not even substitutes; RTK Query by default wraps fetch or can be configured to use your preferred http client. So it's an entirely additional layer of cacheing and refetching logic with a standardized interface around common loading and error states.

It is considerably easier to write performant web applications with a library like RTK Query or TanStack Query than to just use fetch and try to roll your own cacheing solution. I say this as someone who has done all of the above professionally.


In my experience it was due to backend devs (like myself) being asked to or volunteering for some front end projects. At least that is how our current frontend turned into a redux mess and is currently being rewritten.

It is a completely different skillset and I think more often than not people assume that they can write front end code just because they know Javascript.


Redux is very straightforward until you start adding middleware, then the complexities start piling up.


I am a nuts and bolts Python programmer. What the heck even is this?


Why not have it return an async iterable, which has both push and pull semantics? Combined with the iterable helpers proposal you'd get a lot of things for free, including end-to-end backpressure.


Shouldn't this just be like a two line utility function to turn a addEventListner call into an async iterable?


Lol first thing I did was Ctrl+F for async iterator


My "observation" about observability and "reactive type" of code is that old school proxies, events and ways of defining expressions that get computed when the dependecies change is a much simpler way of programm reactive UIs (for example MVVM style of architectures) compared with a functional programming approach. Of course the naive approch does not work properly because any change in dependent values will trigger undesired side effects, reentrace, infinite loops, etc. However in a programming language like JavaScript if the observability is based on a Sound PubSub System these kind of problems reduce to becoming surpinsingly irelevant. If you want to undestand more about this, take a look on https://github.com/OpenDSU/soundpubsub/ We used this approach to implement two ways binding and reactive MVVM web frameworks but this comment is to present this insight that it is possible to have a "procedural" and "spreadsheet like" method of implementing intuitive and sound rectivity without bending your mind with streams or anything very abstract. In a low code environment this could be essential.


Some context about the current status: https://twitter.com/domfarolino/status/1684921351004430336


I only skimmed the document but I didn't see any mention of glitches. If this hasn't been addressed I'm worried there hasn't been sufficient thought about the semantics of Observables. Glitches, and avoiding them in push systems, is addressed in Flapjax (2009): https://www.flapjax-lang.org/publications/

I also don't see why this needs to be part of Javascript when it can be adequately implemented in a library. Today's great idea is tomorrow's legacy.


What are Glitches?


Glitches are transient incorrect values due to delays in propagating values. Imagine a diamond pattern of reactive values like:

    a = 0, 1, 2, ...
    b = a
    c = a
    d = b + c
d should be 0, 2, 4, ... but if, say, b updates and d sees this update before c updates then you can get incorrect values in d.


Quite interesting. How does one resolve this?

Naive me would have d observe a and recalculate its value by recomputing b and c then.

Other naive but complexity indulging me would store a dependency graph and use this graph to determine when one should wait for a value to update.

Surely there must be an easier way?


The key is topological sorting of a dependency graph. This can be done implicitly by storing a reactive variable node’s depth once it is created, and just making sure that updates are enqueued in separate queues per depth.

I have a somewhat small implementation that transparently batches updates with queueMicrotask in this library (bruh): https://github.com/Technical-Source/bruh/blob/a829af9df9405b...


They used to track a dependency map explicitly so they can determine when your “transaction” begins and ends so you can update all dirty at once.

Another way to deal with it is explicitly not dealing with it, eg. saying it is only (inconsistent) UI and will go away in a millisec…


If b, c, and d were lazily evaluated (pull-based) you would also avoid this issue,



What exactly will this benefit from?


which programming language and / or platform ? Is it too obvious to mention it?


ecmascript I have to assume


TC39 (the JS standards body) and WHATWG (the web standards body) have been ping-ponging this back and forth between one another.

I believe WHATWG is being favored so Observables will have a "killer app" (events) and could hence be built into the platform. As I recall, there was friction in TC39 to the effect of "RxJS exists. Why bother specifying this in the language?"


> "RxJS exists. Why bother specifying this in the language?"

And, honestly, that's a great point. It's even easy to wrap EventTargets: fromEvent(target, 'eventName').

For the record: I'm a big fan of RxJS and have used it in production client-side code for several years. But even I'm not hot on this proposal to build it into the language.


js does not need Observable API, it already had Async Iterables that soon will be enhanced with these helpers https://github.com/tc39/proposal-async-iterator-helpers that are similar to this proposal


I like this!


Would absolutely love this.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: