RxJS Alert: toPromise Is Heading for Deprecation
RxJS 7 marks toPromise as deprecated, and RxJS 8 will remove it entirely. This notice is meant to help you get ready for the change before it lands.
What should you reach for instead?
In most cases, lastValueFrom or firstValueFrom will do the job — but read on for the full breakdown.
If you haven't been following along: the majority of us are still running RxJS 6 in production, but RxJS 7 is already out in beta.
Back before RxJS 6 introduced pipe-able operators, toPromise could easily be mistaken for an operator — however, that's not what it is.
It cannot be invoked inside the pipe chain.
Located on the Observable prototype, toPromise is a utility method designed to turn an Observable into a Promise.
Internally, it subscribes to the Observable and resolves the Promise with the final emitted value — but here's the catch — only 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>: This type declaration describes the expected type for the implicitthisobject.
It's known as a fake parameter — existing only during compilation — designed to prevent bugs when a function is passed around and itsthiscontext gets altered without anyone noticing.promiseCtoraccepts a Promise constructor — this allows the method to work with differentPromiseimplementations.- The key part here: the
subscribecall gets wrapped inside aPromise, whichresolves only when theObservable‘scompletehandler fires.
Practical Examples
In the Angular world, most of you will recognize the pattern of handling Observables for HTTP requests through the built-in HttpClient:
@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)
);
}
}
Example of making a HTTP-Request returning a Observable
Using the “old” toPromise
For those who prefer avoiding Observable directly in components, the current approach is 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()
}
}
Example using toPromise
Switching to the “new” lastValueFrom:
From a functionality standpoint, lastValueFrom is the natural successor to 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$);
}
}
Example using lastValueFrom
The syntax await lastValueFrom(categories$) reads quite elegantly, doesn't it?
Embracing the “new” firstValueFrom
With firstValueFrom, stream completion isn't a concern at all. Instead, it grabs the first value emitted, resolves the Promise with it, and unsubscribes immediately.
This comes in handy when dealing with streams that emit continuously and may not complete any time soon.
Imagine consuming server-pushed events, for instance, through SSE — Server Sent Events.
Here, our InventoryService would leverage an [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) to channel each incoming 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();
};
});
}
}
Consuming Server-Sent-Events
If we're after just the first event that arrives, firstValueFrom comes to the rescue:
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$);
}
}
Example using firstValueFrom
What's Behind the Deprecation?
Initially, the reasoning for deprecating toPromise wasn't immediately obvious.
Nicholas Jamieson — a core team member of RxJS — offered some valuable insights to clear things up:
- The goal was to move it off the
Observableprototype and turn it into a standalone utility function. - Its name leaves something to be desired.
This becomes especially apparent when used withawait:
await categories$.toPromise()compared toawait lastValueFrom(categories$) - The type signature for
toPromiseis misleading.
If the sourceObservablecompleted without emitting anything, it resolved withundefined— whereas it should have rejected. APromiseis meant to guarantee a value upon resolution, even if that value isundefined. However, when a stream finishes without ever emitting, there's no way to tell apart a stream that intentionally emittedundefinedfrom one that completed silently. More details can be found here.
In Short
Going forward, steer clear of toPromise and opt for lastValueFrom or firstValueFrom instead.
toPromise gets deprecated in RxJS 7 and will vanish in RxJS 8.
I've deliberately avoided the whole “using Promises is an anti-pattern” debate here.
Feel free to bring it up in the comments section, though.
When would you choose a Promise over an Observable?
One scenario: exposing a Promise-based API to external partners who don't use RxJS at all. It's also handy for async-await patterns in non-marble tests.
Special appreciation goes out to Nicholas and Lars for their contributions — you both inspire both me and the wider community.
Wishing you a productive day — thanks for reading.
