Angular - Promise vs Observable
Older Article
This article was published 8 years ago. Some information may be outdated or no longer applicable.
Since Angular 2 landed, there’s been confusion about when to use promises and when to reach for observables. Here’s how they compare.
Promise
If you’re new to Promises, read an earlier post for an introduction.
Promises handle asynchronous operations and return either a single value (the promise resolves) or an error (the promise rejects).
One thing to remember: a request kicked off by a promise can’t be cancelled. (Some third-party Promise libraries bolt on cancellation, but native JavaScript Promises don’t support it.)
That’s one of the key differences between promises and observables. Since you can’t cancel a promise, an HTTP search triggered on keyup will fire a request for every single keypress.
Here’s a quick example showing the problem:
<!-- app.component.html -->
<div>
<h2>Star Wars Character Search</h2>
<input #term type="text" (keyup)="search(term.value)" />
<hr />
<div *ngIf="results.length > 0">
<li *ngFor="let result of results">
{{ result.name }} is a character with a height of {{ result.height }}.
</li>
</div>
</div>
// app.component.ts
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
private results = [];
constructor(private http: HttpClient) {}
private search(term) {
console.log(term);
this.http
.get(`https://swapi.co/api/people/?search=${term}`)
.toPromise()
.then((data: any) => {
/* tslint:disable:no-console */
console.time('request-length');
console.log(data);
console.timeEnd('request-length');
this.results = data.results;
});
}
}
The console statements are left in deliberately. Here’s what the browser console spits out:
[Log] l
[Log] lu
[Log] luk
[Log] luke
[Log] {count: 2, next: null, previous: null, results: Array}
[Debug] request-length: 0.590ms
[Log] {count: 37, next: "https://swapi.co/api/people/?page=2&search=l", previous: null, results: Array}
[Debug] request-length: 0.281ms
[Log] {count: 1, next: null, previous: null, results: Array}
[Debug] request-length: 0.377ms
[Log] {count: 1, next: null, previous: null, results: Array}
[Debug] request-length: 0.263ms
Four keypresses, four requests. For the search term “luke” that’s four separate API calls. Wasteful.
Promises do have their place. They’re fine for requests you know won’t be cancelled. And don’t forget that Promises work with async/await, which helps keep asynchronous code readable.
Observables
If you’re new to Observables, read this introductory article.
An observable is a stream (of events, or data). Unlike a Promise, an Observable can be cancelled. It ships with operators like map() and filter() out of the box.
Angular uses Rx.js Observables, and it uses them by default when dealing with HTTP requests instead of Promises. That’s why in the previous example we had to call
toPromise()to convert the Observable to a Promise.
Let’s rework the previous example. The goal: make the request cancellable so only one request goes out while the user is still typing.
First change: switch to ReactiveForms. This lets us subscribe to changes on the input box itself:
<!-- app.component.html -->
<div>
<h2>Star Wars Character Search</h2>
<input [formControl]="term" />
<hr />
<div *ngIf="results.length > 0">
<li *ngFor="let result of results">
{{ result.name }} is a character with a height of {{ result.height }}.
</li>
</div>
</div>
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormControl } from '@angular/forms';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
private results = [];
private term = new FormControl();
constructor(private http: HttpClient) {}
ngOnInit() {
this.term.valueChanges
.debounceTime(400)
.distinctUntilChanged()
.subscribe((searchTerm) => {
this.http
.get(`https://swapi.co/api/people/?search=${searchTerm}`)
.subscribe((data: any) => {
/* tslint:disable:no-console */
console.time('request-length');
console.log(data);
console.timeEnd('request-length');
this.results = data.results;
});
});
}
}
Two key methods at work here. debounceTime() discards emitted values that arrive less than 400 milliseconds apart. distinctUntilChanged() only lets a value through if it’s actually different from the previous one.
Subscribe to those changes, fire the API call, subscribe to the results. Check the logs now:
[Log] {count: 1, next: null, previous: null, results: Array}
[Debug] request-length: 0.630ms
One request. That’s it.