Asynchronous programming is hard. Evolution gifted us with a marvelous brain capable of doing many things, but asynchronous and concurrent processes weren't top on the list. We can do it, but only to a certain degree. You should do everything to keep it simple. The async-await pattern is a marvelous tool to reduce complexity.
But what does Angular do? Several years ago, Angular replaced the Promises
of AngularJS 1.x with Observables
. Since then, Promises
are frowned upon. async/await
has been invented later, but from an Angular programmers point of view, async/await
never became a thing. That's a pity, because it simplifies your REST calls tremendously.
By the way, during my research, I learned that Netanal Basal had the same idea five years ago. Maybe I'm not entirely wrong. Another hint is that Aurelia - the framework that tries to be the better fork of Angular - doesn't use Observables
for simple HTTP requests, either. Aurelia still uses Promises
.
TL;DR
This is a long article (once again!), so let's summarize it quickly. In a nutshell, an Observable
is a Promise
with multiple values. But 90% of Observables
in an Angular application deal with simple HTTP calls. 90% of your Observables
never return multiple values. It's only a single value. The power of Observables
is wasted. Simple Promises
are the better tool - especially since the invention of async / await
. The RxJS team has invented the method firstValueFrom()
or - until RxJS 6 - toPromise()
for a reason.
async / await
converts your callback hell into simple, linear code. You don't have to hassle with nested Observables
, mergeMap()
, forkJoin()
, switchMap()
, and mergeMap()
. Instead, you'll write almost the same code your Spring backend uses. One line is executed after the other, without nasty surprises. But - that's the different to Spring code - it's still non-blocking I/O. The best of two worlds!
When I suggest that, I usually hear that you'll loose a lot. You can cancel a Promise
, nor can you set a timeout, let alone retry the call when it fails, and you'll generate a memory leak because the Observables
are not unsubscribed. That's true, and at the same time it's nonsense. I don't want you to throw RxJS into the dustbin. Quite the contrary. When you're converting a httpClient.get()
REST call into a Promise
, it's still an Observable
at heart. Here's how to have the cake and to eat it, too:
Once you've got there, all the other advantages of Observables
stop being advantages. I'll show that most operators of RxJS make your code more convoluted instead of making it more manageable.
Does my example look verbose? Yeah, but I guess that's because you never cancel your REST calls, nor do you add a timeout or retry them. FWIW, I've hardly ever seen any of them in the wild. That's another fun fact: when developers tell you they prefer Observables
because of these features, ask them if they are using them. You'll be surprised. Nonetheless, it's a good idea to use them, and it's easy to define a function covering the long pipe and the toPromise()
or firstValueFrom()
call.
Don't get me wrong. I love RxJS. An Observable
is just great when you're dealing with a stream of events. All I'm saying is: let's choose the right tool for the job. If your Observable
is only a glorified Promise
, convert it to a Promise
.
What's the thing about async/await?
First of all, as much as I love the async / await
pattern, it's not a silver bullet. Writing asynchronous code is hard, and it remains hard. You can't use async / await
without knowing what a Promise
is. It took me some time to wrap my head around Promises
. And I still haven't understood it perfectly. For example, how to iterate a folder using the asynchronous version of fs.readdir()
? Granted - that's easy, but only until you want to do it recursively. Implementing asynchronous code with callbacks recursively is a skill you have to learn.
Let's have a look at the recursive folder task. It's not an Angular example, but it shows how challenging callback functions are.
Here we go. I did what every good programmer does: I googled and found an answer at StackOverflow. Here's how the StackOverflow experts list a directory recursively:
const fs = require('fs'); const path = require('path'); function walk(dir, done) { let results = []; fs.readdir(dir, function(err, list) { if (err) return done(err); var pending = list.length; if (!pending) return done(null, results); list.forEach(function(file) { file = path.resolve(dir, file); fs.stat(file, function(err, stat) { if (stat && stat.isDirectory()) { walk(file, function(err, res) { results = results.concat(res); if (!--pending) done(null, results); }); } else { results.push(file); if (!--pending) done(null, results); } }); }); }); };Honestly: I couldn't write that. Or it'd take me forever. It's not enough to call the walk()
function recursively. You also need the result
array and the pending
counter. Both are crucial to make this algorithm work. I guess it's merely a pattern, a skill I can learn - but it's a skill I don't want to learn. More to the point: I don't want to force my co-workers to learn this skill. There must be an easier way!
Let's have a look at the same algorithm using async / await
:
Wow. Eight lines instead of 22 lines. Plus, it's easy as pie. It almost looks like its synchronous counterpart.
That's the beauty of async / await
. It's still asynchronous code. But it reads and writes like synchronous code. Usually, it's also shorter.
Observables are more powerful than Promises
At this point the average Angular architect usually says "Yes, but...". They list a long list of "buts", of things Observables
do better than Promises
. They most popular "but" is:
“Observables
can deal with streams of events.Promises
can only deal with one single event.”
Yeah. Dealing with streams of events is pretty cool when you need it. But you hardly ever do.
Have a look at your application. How many of your Observables
return more than one result?
I bet the vast majority of your Observables
are simple REST calls. That's one result from the server. One. At most. It's either a success or an error message. REST calls don't deal with streams of events. 90% of your application doesn't. You need Observables
only for the remaining 10%.
In other words, the majority of RxJS operators is useless to you. You'll never filter()
a stream, nor will you skip()
some of the messages of the stream, nor will you zip()
to streams of events.
Single events can indeed be considered a special - just very short - stream of events. When the Angular team decided to drop Promises
in favor of RxJS, they had a point. Both APIs use callback functions. Both APIs have similar goals. So it makes sense to use a uniform API. Or at least, it made sense at the time because async / await
had yet to be invented. As far as I remember, async / await
made it to the JavaScript world one or two years later.
The dirty secret is async / await
is a game-changer. It's the tool of choice for REST calls.
Cancelling Observables
Only it isn't, they say. You can't cancel a Promise
. Embracing Promises
you can never cancel your REST calls.
Before answering that, let's have a look at why you might want to cancel a REST call.
Imagine your user opens a page of your application and leaves it before the server responds to the request. In this case, you'll want to cancel your REST call. You're not interested in the response. That's easy with the httpClient.get()
. It cancels the Observable
automatically.[1] However, there's no way to cancel a Promise
. When the server sends its response, the callback function of your Promise
is executed. That's dangerous. Chances are the garbage collector has already removed the data structures your Promise
callback needs. So you end up with mysterious error messages. Even if your application doesn't run into errors, it still executes code that's no longer needed. That's a waste of resources.
But is this true?
No, it's not. Not with the approach I've got in mind. I don't suggest to throw Angular's httpClient
into the dustbin. Instead, I want to wrap calls like httpClient.get()
in a Promise
façade like so:
Found ${bookshelf.length) books in my bookshelf
);
If you're still using RxJS 6, you need to use toPromise()
instead of firstValueFrom()
:
Both toPromise()
and firstValueFrom()
support cancellation. They are just shallow wrappers around the Observable
. Being the curious guy I am, I've read the RxJS source code, just to be sure. firstValueFrom()
throws an EmptyError
when the underlying Observable
is cancelled. As long as you catch your exceptions you can deal gracefully with the ngOnDestroy
challenge. If the user leaves the page before the server responds, the request is cancelled, and your catch block can deal with it.
Problem solved!
Cancelling http request programatically
Well... not quite. It always pays to listen to Pascal Precht. Imagine you were to implement the Google search box. Every time the user hits a key, a search is triggered. Many of us type faster than our backend responds. So you trigger many server requests, but you're only interested in the last response. You'll want to cancel the other REST calls. Adding insult to injury, you must cancel the outdated REST calls because the server may respond out-of-order.
There's that. It's an exotic use-case, but still, it's a valid use-case. It's also a use case you should avoid at all costs. When you cancel a REST call, that only means you cancel it on the client-side. The server still processes the REST call. If you're worried about climate change (or your AWS bill or server-side performance), avoid that. Add a debounce()
to the Observable
triggering the REST call.
Come on, that's an interesting (if slightly exotic) scenario. Among other things, it shows that Observable
can peacefully - even fruitfully! - co-exist with async / await
. I've modified an example I've found at www.freakyjolly.com:
http://www.omdbapi.com/?s=${term}&apikey=${APIKEY}
;
const params = { params: PARAMS.set('search', term) };
return firstValueOf(this.httpClient.get(url, params));
}
}
This search box goes to great lengths to prevent the scenario Pascal Precht describes. Your search keyword requires at least three letters, there's a long debounce time of one second - which means that there's at least one second between consecutive server calls, and even more, if you hit a key twice a second - and it even adds the nice distinctUntilChanged()
operator.
RxJS rocks!
The async / await
block comes next. It only deals with the REST call. That's the call you want to cancel. Maybe you've set the debounce time lower, or your network is extraordinarily slow, so OMDBAPI takes more than one second to respond. Can we cancel the request programmatically when the user hits the next key?
Yes, we can!
Actually, that's merely pimping up the REST call:
return firstValueOf(this.httpClient.get(url, params).pipe( takeUntil(this.cancelPreviousRESTCalls$) ));Oops, that was probably a bit fast. Let's walk you through it step-by-step.
Actually, it's easy. You can cancel any Observable
using takeUntil()
. It doesn't make any difference your've wrapped the Observable
into a Promise
. You can still use takeUntil()
. We've already seen that a minute ago: When the Observable
is cancelled, an EmptyError
is risen.
This is what the complete solution looks like. Take it with a grain of salt. It's pseudo-code, possible including syntax errors, but the idea is still valid:
@Component({... }) export class AppComponent implements OnInit { @ViewChild('movieSearchInput', { static: true }) public movieSearchInput: ElementRef; private apiResponse: Arrayhttp://www.omdbapi.com/?s=${term}&apikey=${APIKEY}
;
const params = { params: PARAMS.set('search', term) };
return firstValueOf(
this.httpClient.get(url, params).pipe(
takeUntil(this.cancelPreviousRESTCalls$)
));
}
}
In a nutshell, all you need to do is implement cancelPreviousRESTCalls$
and catch the exception. You end up with a version that's as lengthy as the pure RxJS solution. Sorry about that. On the plus side, you've got one callback less. One thing less to wrap your head around.
Maybe that's not the simplification I've promised you. But that's OK. We've examined an exotic use case. I still think using async / await
is a good choice, even in this case, but I also think it's also OK to use pure RxJS. It's your choice.
The other point I'd like to bring home is that canceling a REST call is evil. You can't stop the server from processing the event. That's a shortcoming of the HTTP protocol, but at the time being, that's just the way it is.
Memory leaks
Time for the next "but." What happens if the server never answers?
If you're using an Observable
, that's not a big deal. You're collecting every Subscription
and unsubscribe them in the ngOnDestroy
method. At least I hope so. I've covered this in detail some time ago.
That's something you can't do with a Promise
. Or is it?
Well, first, I don't believe you unsubscribe your REST calls. Because HttpClient
always calls complete
when the server response arrives. That's something you can rely on. The only exception is when the server never sends a response. So common wisdom is not to unsubscribe calls initiated by HttpClient
. You can unsubscribe from the subscriptions, but that's only for the paranoid. More often than you, you'd end up unsubscribing from a subscription twice.
Plus, it's not true you can't unsubscribe a promisified httpClient.get()
. There's an RxJS operator for that. You've already seen it: takeUntil()
is your friend. I also recommend adding a timeout. That's adding to the code I want to simplify - but hey, I didn't promise you a rose garden. Now the code looks like so:
I've shown most of the takeUntil()
approach above. Now I recommend you also call cancelPreviousRESTCalls$.emit()
in the ngOnDestroy()
method.
Retrying http calls
This is another popular "but" I've hardly ever seen in the wild. Supporters of RxJS insist that RxJS makes it easy to implement a retry mechanism. If the server fails to answer immediately, try several times again. Maybe the next request succeeds. Using RxJS, that's a walk in the park:
http.get(url) .retry(3) .subscribe(...);Another, more sophisticated example looks like so:
http.get(url) .retryWhen(attempts => { return Observable .range(1, 3).zip(attempts, (i) => i) .flatMap(i => Observable.timer(i * 1000)); });Or, just another example. How do you like this? Isn't it easy?
this.http.get('./country-info/' + country + '.json') .retryWhen(error$ => error$.switchMap(err => navigator.onLine ? timer(1000) : fromEvent(document, 'online')) // ONE LINE ADDED .map((res: Response) => res.json()) .subscribe(res => this.capitol = res.capitol);There's no doubt about that: all these examples rock.
And none of them is saying that toPromise()
or firstValueFrom()
are bad. What about this solution?
It's just that easy!
Promises trap errors
Here's another popular point against using Promises
. They say Promise swallow errors.
So what? A close look reveals that the complete statement is "Promises swallow errors unless followed by a catch block."
Using async / await
, that's always true. await
converts both exceptions and rejected promises to an exception. Actually, using async / await
is a good way to make sure errors are never silently trapped.
Promises are always asynchronous while Observables are synchronous
I love this "but". Because it's sort of true. There's a fundamental difference between Promises
and Observables
. An Observable
is executed synchronously. A Promise
is not. The callback method of a Promise
is always added to the microtask queue.
Isn't that a disadvantage? If we're using Promises
, do we lose control over our application? Do we end up with a plethora of threads?
Not quite. Many - even experienced - developers talk about threads in JavaScript. But fear not: There's no such thing in JavaScript.[2] JavaScript works exactly the way you do in real life. There's a pile of to-dos on your desktop. Each time your boss looks by, you add a couple of to-dos to the top of the pile. JavaScript does something similar. When you call a Promise
, your callback is added to the pile of microtasks.
It's true that Observables
don't do that. But in this article, I'm specifically talking about REST calls. httpClient
calls are inherently asynchronous. The response always arrives with a delay. It's processed as one of the delayed tasks of the microtasks queue.
So there's almost no difference if you call your REST call as a Promise
or an Observable
. Plus, my approach is to wrap the Observable
in Promise
. So there's no difference at all.
Just enjoy the simplified API!
Observables have many operators Promises don't have
Come on. I've already sold it to you, haven't I?
"No, no, no," the RxJS fans say, "you're about to lose all those useful RxJS operators"!
That's a cool "but." It sounds true, and it resonates with me. But still, it's rubbish.
Sorry, guys. RxJS has to offer many powerful operators for a reason. It needs them to pull you out of the callback hell it creates. The RxJS operators solve problems you don't have without RxJS.
Does that sound a bit harsh? Well, first of all, let me remind you I'm only talking about REST calls. Second, I'll prove my point with source code.
Three common scenarios cross my mind:
- A single REST call.
- Two consecutive REST calls. The second REST call needs data provided by the first REST call.
- Two parallel REST calls. You need both responses to proceed.
If it were only for the first scenario, I wouldn't write this article. That's fairly simple, no matter if you're using Observables
, classical Promises
or contemporary async / await
.
Two consecutive REST calls depending on each other
Two consecutive REST calls are nasty with Observables
because you're nesting Observables
. You need the switchMap()
operator to get out of this mess. Being a seasoned Angular developer, you know the drill:
https://example.com/book/content/${isbn}
),
map(url => httpClient.getThat happens all the time. Nested Observables
are bread-and-butter business in the Angular world. Operators like switchMap()
and mergeMap()
come natural to most Angular developers.
However, await
avoids the problem altogether. You'll never deal with nested Promises
. await
is sort of the equivalent of switchMap()
. It's a lot more intuitive and straight-forward. Just look at the source code:
https://example.com/book/content/${isbn}
;
const firstBookContent: string =
await firstValueFrom(httpClient.getAs you can see, you're dealing with the base types. There are no callbacks, and you don't have to wrap your head around an abstraction like Observable
. Programming is fun again!
Parallel REST calls
Promises makes it particularly easy to call multiple REST calls in parallel. Just use Promise.all()
. If you need to support older browsers, there's a polyfill for that. Let's go:
https://example.com/user/${userName}
);
const [bookshelf, userData] =
await Promise.all([bookshelfPromise, userDataPromise]);
// now both bookshelf and userData are available
Of course, there's a RxJS operator covering this usecase:
import {Observable} from 'rxjs/Observable'; Observable.forkJoin( this.http.gethttps://example.com/user/${userName}
)
).subscribe(response => {
const [bookshelf, userData] = response;
});
In this particular case, the RxJS version doesn't look that convoluted. I still prefer using Promises
. await
allows me to deal with the real data types.
Chaining RxJS operators
Chaining operators is strangely attractive to developers. That even includes me. It feels good to break the algorithm into small chunks, each in a single line, linked by fancy RxJS operators. The resulting algorithm looks like you went into great length to make it simple. RxJS offers a wide range of powerful operators to make your life simple.
The dirty little secret is that this isn't true. Most RxJS operators solve problems caused by RxJS. Granted, we've already seen there are useful operators, such as retry()
, skip()
, takeUntil()
, and timeout()
. By now, you know you can easily replace forkJoin()
by Promise.all()
, and you never miss switchMap()
when you're using async / await
. Let me add just one single example. What happens to our beloved map()
operator?
Here's the traditional RxJS code:
const url = './country-info/' + country + '.json'; this.http.getOne map()
and a tap()
. How cool is that!
That's a bit anticlimactic, isn't it? The map()
has gone away for good. Now it's a simple assignment. The tap()
operator is gone, too. Just the body of the closure called by tap()
remains.
Working with arrays
Working with arrays is a real pain the ass if you're using Observables
. More often than not, the server sends an array of data. Of course, you can deal with the array using RxJS. The thing is, RxJS uses almost the same operators you also need to process arrays. If you love functional programming, that is. So I often end up with two map()
statements in the same line. To me, that's not a big deal. I've written the code. I know that the first map()
is an RxJS operator and the second map()
belongs to arrays.
But every time I try to teach Angular to developers with a Java background, they're confused. Distinguishing between the two map()
functions and detecting which map()
plays which role is a skill you have to learn. Async / await
takes the sting out of it. It simply remove the RxJS map()
from the equation. We've already seen that a minute ago: map()
is replaced by a simple assignment.
Embracing await
converts the same algorithm to a walk in the park:
Lazy execution
They say lazy execution is a big plus of Observables
. I guess that's true. The funny thing is, I can't remember a single time I've needed lazy evaluation. My average use case is that my application needs data from the server, and it can't proceed without this data. Lazy evaluation is exactly the opposite of what I need. I don't want my application to wait before fetching the server data. I need the data now, and there's no point in deferring the REST call.
I wonder when evaluating a REST service lazily is an advantage. Remember, I'm only talking about REST service. Generally speaking, lazy evaluation is a great tool. But this article is specifically about REST services. I suppose you call your REST service when you need them. If so, you'll never execute your observables lazily. Promises do the trick just as well.
A single programming paradigm is better then needing two paradigms
When the Angular team decided to drop Promise
in favour of Observable
, both approaches were using callback functions. It doesn't matter if you have to wrap your head around Promises
or Observable
. It's pretty much the same learning curve. At the time, it was a good decision to drop Promises
in favor of Observables
. When both approaches are equally painful, it makes sense to adopt the broader use-case.
However, async / await
is a game changer. And you've already seen that I don't condemn Observables
. Quite the contrary, I suggest combing the power of Observable
with the simplicity of async / await
.
From this point of view, I'm convinced it doesn't make sense to use a single pattern. Let's use async / await
for the simple cases because it matches the way our brain works. At the same time, let's enjoy the power of Observables
.
What about the built-in fetch API of the browser?
An attractive alternative to using Angular's HttpClient
is using the built-in API of the browser. Actually, I encourage you to examine it. But at the time of writing, it's not perfect. Most of the popular arguments against using firstValueOf()
are valid if you're using fetch
. For instance, you can't cancel a fetch()
call.
So, no, don't use the native API of the browser. Not yet, that is. The fetch()
API is still evolving. When stuff like timeouts and cancelling lands, using the native API of the browser is going to be the way to go. Until then, stick with Observables
and decorate them with a cute little firstValueFrom()
or toPromise()
.
Eat your own dog food
I'm not there yet - but after thinking so long and so hard about it, I've decided to replace the Observables
of my blog with async / await
. The first few tests were very successful. I'm going to eat my own dog food. If it's a failure, I'm going tell you. But this experiment will work!
Wrapping it up
I'd like to hear from you. There must be a reason why the Angular community sticks with Observable
. It's weird that even influencers like Netanal Basal suggest using toPromise()
, only to be ignored.
I believe it's just the momentum of embracing RxJS (plus being told that RxJS is superior). But I'm always open to new ideas. If you've got a good reason why using httpClient.get().toPromise()
is a bad idea, please drop me an email at articles@beyondjava.net. Just remember, I do love RxJS because of its power. If you believe I'm wrong, and if you want to convince me, I'd appreciate you covering REST calls because that's what this article is about.
In the meantime, I'll enjoy the beauty of pseudo-linear code.
Dig deeper
Stop using observable when you should use a promise by Netanel Basal
JavaScript Theory: Promise vs Observable
Do Observables make sense for http?
toPromise() is being deprecated (in favor of firstValueFrom)
How to implement an interceptor to set a timeout globally
Iterating a folder structure recursivly
- At least I'm told it's canceled automatically. I haven't run a test yet. If you want to be sure, unsubscribe the event in the
ngOnDestroy()
method using one of the approaches I've described at https://www.beyondjava.net/unsubscribing-from-an-observable-in-angular.↩ - except for Service Workers.↩