This is a common question when you first get into Angular development. What is an Observable
and how are they different from Promises
? It is also an excellent question that highlights the difference between simply dealing with asynchrony and turning asynchrony into a powerful tool of reactivity. This in fact is the fundamental difference between promises and observables. One handles individual asynchronous calls the other provides for an entire platform for rich, functional & reactive programming.
I Promise…
Promises are a well-established facility, one might call it a primitive, for dealing with asynchronous operations in Javascript. A promise is just that: an assurance that if and when an asynchronous operation completes, regardless of whether it completes successfully or not, you will get the result and have an opportunity to handle it.
Why Promises?
It may help to put promises into context, before we dive into the differences between a Promise and an Observable. Before promises, we had callbacks as a way to deal with asynchrony. Callbacks had hell; the hell of deep nesting, callback passing and abstractions, high coupling, high complexity, poor testability, poor maintainability, etc. Promises provide a cleaner mechanism to deal with asynchronous behavior, allowing us to break that asynchrony down into smaller, simpler, more testable, more maintainable units with looser coupling.
In short, Promises allow us to go from this (which is a very mild case with greatest simplicity):
funcA(input, (err, resultA) => { err ? console.log(err) : funcB(resultA, (err, resultB) => { err ? console.log(err) : funcC(resultB, (err, resultC) => { err ? console.log(err) : funcD(resultC, (err, resultD) => { err ? console.log(err) : funcE(resultD, (err, resultE) => { err ? console.log(err) : console.log(resultE); }); }); }); }); });
To something like this, which takes high complexity and deep nesting and converts it to something flat with much lower complexity and higher clarity:
funcA(input) .then(funcB) .then(funcC) .then(funcD) .then(funcE) .then(resultE => console.log(resultE)) .catch(err => console.log(err));
Promises also increase our ability to decompose code into smaller units, and recompose those units later on:
const promiseB = funcA(input).then(funcB).catch(console.log(err)); // ... const promiseE = promiseB.then(funcC).then(funcD).then(funcE).catch(console.log(err)); // ... promiseE.then(resultE => console.log(resultE));
Both of these approaches are solutions to handling asynchronous code. One, however, is significantly better from the standpoint of code complexity; from the standpoint of simplicity, readability, maintainability, testability, composability, and all the other “ilities.” Promises simplify asynchronous code.
What does a Promise do?
A Promise
is a construct that allows us to handle the single result of a single asynchronous process as well as a construct that allows us to manage an asynchronous process. A Promise
handles a single result, from a single asynchronous process, by calling a success handler, or an error handler, when the asynchronous process has completed.
Promises are Once-and-Done facilities. One hit wonders.
One Thing at a Time
It should be clearly noted that Promises handle single results. This may be something like making a call to a REST API; when that call completes (regardless of whether it succeeds or fails), your code can be notified of that completion and continue executing.
A REST call is the perfect use case: you initiate a connection, make a request, wait for a response, receive the response, and when the response has been fully read or the channel throws an error, you close the connection. There is a definite beginning, an asynchronous waiting period and a definite completion.
import * as request from 'request-promise'; export class TimeService { getTime(timezone: string): Promise,[object Object]
A Promise in this context will immediately return an object that can be used to wait for completion and when that completion occurs hand control back to the developer along with the resulting data or any resulting error if an exception occurred. Once completion has occurred, a Promise will provide the result but cannot be used to handle any subsequent results nor can it be used to re-initiate the original asynchronous process.
Incompatible Use Cases
In contrast, a WebSocket would not be a good use case for promises. Where a REST call has definite completion, a WebSocket is a continuous feed of information until it is closed (if it is closed). If a Promise
was used with a WebSocket, only the first result will be captured by a Promise (most likely, only the conclusion of opening the connection, depending on the WebSocket library used), at which point it would be completed and no longer useful.
WebSockets represent a streaming context where many notifications, one after the other, may be emitted over time until the socket is closed or errors out, resulting in completion. WebSockets present an ideal segue into Observables and reactive programming.
Let’s Observe…
While they may seem “new”, in fact observables have actually been around for quite some time. Rx, or ReactiveX (which stands for Reactive Extensions), were actually introduced by Microsoft for .NET/C# circa 2008/2009 (which actually makes them older than Promises).
ReactiveX for .NET presented a way of using a reactive paradigm for programming and paved the way for libraries following the same API and paradigm to be made for most other major languages including Java, JavaScript/TypeScript, Python, etc. for developing with reactive extensions. This is the foundational concept of Observables.
Similar to promises, observables provide a mechanism for dealing with asynchronous behaviors. They allow us to wait for a result and when a result occurs they hand control back to the developer to handle the result: success or failure. In some use cases an Observable may be used just like a Promise, once-and-done style, where a process is initiated, does work, succeeds or fails, and completes.
In other cases, observables, and RxJs in general, provide a significantly richer platform for handling all manner of asynchronous behavior, whether they “complete” or not, continuous streams or discrete events.
Single then Complete
Observables can be used in very Promise-like use cases, and can therefore replace them entirely. Back to our earlier example of a REST API call: we have a definite beginning, a waiting period while background work is done, then a result that may be the successful return of data or an error and finally completion.
As you can see here, using an Observable
for an asynchronous REST API call is actually quite similar to using a Promise. You can handle your successes and failures, just like a promise. Unlike promises, in the land of observables these concepts (success/data, errors, and completion) are first class and realized concretely within RxJs itself through Notification
s.
import { HttpClient } from '@angular/common/http'; export class TimeService { constructor(http: HttpClient) {} getTime(timezone: string): Observable,[object Object]
Completion is an important concept with observables: this gives them the opportunity to clean themselves up as well as give the developer the opportunity to handle completion for their own purposes. Completion will automatically dispose of resources used by an observable. (In the case of Angular’s HttpClient service as seen above, all observables returned by methods of this class complete after the request has returned.)
Streams
This single-instance once-and-done use case will be the most familiar to those coming from promises. It should be made clear, however, that these use cases barely scratch the surface of what observables can do, especially when leveraging the full power of RxJs and all of its operators and extensibility. Before we dive deeper into RxJs, let’s cover the use cases that can be handled with observables, where promises were insufficient.
The WebSocket example from earlier is a great example of a use case perfect for observables. A WebSocket represents an open channel of communication, where data may be delivered in many discrete events, serialized one after the other. A WebSocket, once opened, may effectively remain open forever (representing a true continuous stream) or may at some point be closed (representing a natural completion.)
In the case of a WebSocket we may have many values, and possibly even many errors, being emitted by the socket. In RxJs, such a source of streaming information is often called a Subject
which is also a first class concept surfaced to the developer through concrete classes. Subjects are observable but also provide facilities to issue notifications as well.
import { fromEvent } from 'rxjs'; import { webSocket } from 'rxjs/webSocket'; const socket = webSocket('ws://echo.socketserver.com:8081'); // Handle messages from server (echos of messages to server) socket.subscribe({ next: message => console.log(message), error: error => console.error(error), complete: () => console.debug('Socket has closed.') }); // Send messages to server fromEvent(btnMessage, 'click').subscribe({ next: () => socket.next({message: input.value}); }); // Close the web socket fromEvent(btnClose, 'click').subscribe({ next: () => socket.complete(); });
The richer API of Observables (and on a larger scale, of RxJs) with first-class representation of all three of the possible outcomes (next, error, complete) from any discrete event allows us to handle continuous streams (and significantly more complex scenarios) with observables, where promise are incapable of providing a solution.
A more complete view of the concepts driving an observable stream of notifications from some source context
Operators
Moving beyond the basics of how Observables function, their real power and flexibility ultimately lies in the pipe and the operators you can use therein. Pipeable operators provide a rich, functional means to react to, handle, transform, filter, branch, fork, join and otherwise manage streaming data. RxJs ships with a fairly extensive library of ready-made, standardized operators while also allowing developers to implement their own.
Standard operators include map
for transforming one kind of input to another kind of output, tap
to perform side effects, filter
to control which notifications propagate farther through the pipe and many more. Operators include the ability to throttle, audit or debounce the frequency/rate of notifications, to perform more advanced filtering, to scan or reduce or even control the flow of streams through signals from other streams (i.e. takeUntil
, takeWhile
, etc.). There are operators to combine or join streams and operators to branch.
In addition to operators, RxJs provides a set of factory functions that may be used to create Observable streams. Factory functions such as of
, from
, timer
, interval
and others are fundamental to converting static or imperative code into reactive code. The from
factory function can be used to convert Promise
s into Observable
s.
Finally, RxJs provides several ways for developers to create their own operators, including a free pipe
function. Custom operators provide a base platform upon which highly functional and reactive applications may be built with JavaScript.
Conclusions
Promises and Observables have some things in common. Both allow us to deal with the asynchronous nature of JavaScript, but in a better way than the original, inherent, and classic nested callback handler approach.
Promises allow us to handle individual asynchronous events, chain asynchronous calls together and simplify error management. However promises fall short of providing a comprehensive enough solution to handle the not-so-uncommon use cases where we may need more than one value from an asynchronous source.
Observables are not only able to solve the same problems that promises do but they can handle the continuously streaming cases as well and provide a very rich platform with an extensive library of tools to handle any scenario you may encounter while developing asynchronous software with JavaScript.
The journey into reactive programming doesn’t stop once you have learned all the built-in operators, either. RxJs allows the creation of custom operators which may be used within the observable .pipe()
, and thanks to the free pipe()
function, a whole new world of truly functional programming that reacts to changes in streams of data and events will open up before you.