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 valuebut here's the catchonly 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>;
  }

toPromise Source

  • this: Observable<T>: This type declaration describes the expected type for the implicit this object.
    It's known as a fake parameter — existing only during compilation — designed to prevent bugs when a function is passed around and its this context gets altered without anyone noticing.
  • promiseCtor accepts a Promise constructor — this allows the method to work with different Promise implementations.
  • The key part here: the subscribe call gets wrapped inside a Promise, which resolves only when the Observable‘s complete handler 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 SSEServer 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:

  1. The goal was to move it off the Observable prototype and turn it into a standalone utility function.
  2. Its name leaves something to be desired.
    This becomes especially apparent when used with await:
    await categories$.toPromise() compared to await lastValueFrom(categories$)
  3. The type signature for toPromise is misleading.
    If the source Observable completed without emitting anything, it resolved with undefined — whereas it should have rejected. A Promise is meant to guarantee a value upon resolution, even if that value is undefined. However, when a stream finishes without ever emitting, there's no way to tell apart a stream that intentionally emitted undefined from 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.


Further reading: Interoperability between Promises and RxJS