RxJS heads up: toPromise is being deprecated
In RxJS 7 toPromise will become deprecated and with RxJS 8 it will be gone! So avoid using toPromise in your future development when possible.
In RxJS 7 toPromise
will become deprecated and with RxJS 8 - it will be gone! This is a heads up that tries to prepare you for this breaking change.
What can we use instead?
In short: lastValueFrom
or firstValueFrom
- but read on to get the full picture.
In case you have missed it: Currently most of us are enjoying our reactive ride on RxJS 6, but RxJS 7 is already available as a beta version.
Before RxJS 6 and the introduction of pipe
-able operators we could have mistaken toPromise
as an operator, but - it is not.
It can't be used within the pipe
function.
The toPromise
function lives on the prototype of Observable
and is a util method that is used to convert an Observable
into a Promise
.
Inside this function we subscribe to the Observable
and resolve the Promise
with the last emitted value - attention - when the Observable completes!
toPromise<T>(this: Observable<T>): Promise<T>;
toPromise<T>(this: Observable<T>, PromiseCtor: typeof Promise): Promise<T>;
toPromise<T>(this: Observable<T>, PromiseCtor: PromiseConstructorLike): Promise<T>;
toPromise(promiseCtor?: PromiseConstructorLike): Promise<T> {
promiseCtor = getPromiseCtor(promiseCtor);
return new promiseCtor((resolve, reject) => {
let value: any;
this.subscribe((x: T) => value = x, (err: any) => reject(err), () => resolve(value));
}) as Promise<T>;
}
this: Observable<T>
: Type declaration of thethis
parameter which is describing the expected type of the implicitthis
object.
This is a so-called fake parameter - which only exists at compile time - to avoid bugs when you pass a function around and change itsthis
context without noticing it.promiseCtor
is a Promise constructor - enabling us to use it with differentPromise
implementations.- The important part: We are then wrapping the the
subscribe
call in aPromise
which willresolve
when theObservable
'scomplete
handler is called.
Examples
When it comes to Angular most of you will be familiar dealing with Observables
when we make HTTP requests using the built in HttpClient
from Angular:
@Injectable({
providedIn: 'root'
})
export class InventoryService {
constructor(private httpClient: HttpClient) {}
getCategories(): Observable<Category[]> {
const url = 'https://www.themealdb.com/api/json/v1/1/categories.php';
return this.httpClient.get<CategoriesResponse>(url).pipe(
map(response => response.categories)
);
}
}
Usage of the "old" toPromise
When we don't want to deal with a Observable
in our component then we can currently use toPromise
:
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
categories: any[];
constructor(private inventoryService: InventoryService) {}
public async loadCategories() {
this.categories = await this.inventoryService
.getCategories()
.toPromise()
}
}
Usage of the "new" lastValueFrom
:
From a functional perspective lastValueFrom
is the function we should go for as replacement of toPromise
.
import { lastValueFrom } from 'rxjs';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
categories: any[];
constructor(private inventoryService: InventoryService) {}
public async loadCategories() {
const categories$ = this.inventoryService.getCategories();
this.categories = await lastValueFrom(categories$);
}
}
The change to await lastValueFrom(categories$)
reads pretty nice, don't you think?
Usage of the "new" firstValueFrom
When we want to use firstValueFrom
we are not interested in the completion of the stream at all. What we want is its first emitted value, resolve the Promise
with it and unsubscribe from the stream.
This can be useful when we are dealing with a stream that is constantly emitting values and is not completing immediately.
As an example we could think of consuming a stream of events - pushed by the server - e.g using SSE's - Server Sent Events.
Then our InventoryService
would use a EventSource
to pass every received message into an Observable
stream.
@Injectable({
providedIn: "root"
})
export class InventoryService {
constructor() {}
getCateogries(): Observable<Category[]> {
return Observable.create(observer => {
const eventSource = new EventSource('https://www.themealdb.com/api/categories/events');
eventSource.onmessage = event => {
observer.next(event);
};
eventSource.onerror = error => {
observer.error(error);
};
return () => {
eventSource.close();
};
});
}
}
When we are only interested in the first event that is arriving, we can now make use of firstValueFrom
:
import { firstValueFrom } from 'rxjs';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
categories: any[];
constructor(private inventoryService: InventoryService) {}
public async loadCategories() {
const categories$ = this.inventoryService.getCategories();
this.categories = await firstValueFrom(categories$);
}
}
Why is this happening to me?
At first sight it wasn't totally clear why toPromise
is being deprecated.
Nicholas Jamieson - core team member of RxJS - helped us out here and provided us with some very insightful information:
- One goal was to remove it from the
Observable
prototype and turn it into a standalone util function. - The naming of
toPromise
is not the best.
Especially when used in combination withawait
it does not read very well:await categories$.toPromise()
vsawait lastValueFrom(categories$)
- The type information of
toPromise
is wrong.
When the sourceObservable
completed without ever emitting a single value - it resolved withundefined
. It should reject in that case.
APromise
is a "promise" that when it resolves a value will be there - and be itundefined
. But when the stream completes without ever emitting a value you can't differentiate between a stream that a emittedundefined
on purpose and a stream that completed without ever emitting anymore. More on that here.
Summary
Avoid toPromise
for future development and prefer the use of lastValueFrom
/ firstValueFrom
.
toPromise
is being deprecated in RxJS 7 and will be removed in RxJS 8.
I tried to avoid the "using Promises is a anti-pattern" topic on purpose here.
Feel free to discuss it in the comments, though.
When would you use Promise
over Observable
?
I can think of an API lib that additionally exposes Promises to external partners, who may don't use RxJS at all. It's also useful when using async-await in non-marble tests.
Special thanks to Nicholas and Lars for the help with this article - you both are a great inspiration for myself and our community.
I wish you a great day - thanks for stopping by.
Further reading material on the interoperability of Promises and RxJS