skip to content

In RxJS 7, why was Observable.toPromise() deprecated, and how do firstValueFrom and lastValueFrom behave on empty or endless streams?

level: middleimportance: should knowfreq 48%

answer

  1. which value did toPromise pick
  2. undefined on an empty stream
  3. EmptyError unless a defaultValue
  4. a source that never completes

basics

~20 s

toPromise() hid which value it returned and resolved undefined for an empty stream. firstValueFrom resolves on the first value and unsubscribes; lastValueFrom waits for completion. Both reject with EmptyError on an empty stream unless given defaultValue, and hang forever if nothing arrives.

solid answer

~40 s

`toPromise()` resolved with the **last** value on completion, which its name never said, and for a stream that completed without emitting it resolved with `undefined`, so RxJS 7 typed it `Promise<T | undefined>` and deprecated it for removal in v8. The replacements make the choice explicit. `firstValueFrom(source$)` resolves with the first value and immediately unsubscribes. `lastValueFrom(source$)` resolves with the final value once the source completes. Both reject with the source's error, and both reject with `EmptyError` if the source completes empty, unless you pass `{ defaultValue }`. Neither can help with a source that never emits and never completes: the Promise just never settles, so add `take`, `timeout` or similar upstream.

code

ts · 17 lines
ts
import { EMPTY, interval, firstValueFrom, lastValueFrom, take } from 'rxjs';

async function demo() {
  const first = await firstValueFrom(interval(1000)); // 0, then unsubscribes

  const last = await lastValueFrom(interval(1000).pipe(take(3))); // 2

  const fallback = await firstValueFrom(EMPTY, { defaultValue: 'none' }); // 'none'

  try {
    await lastValueFrom(EMPTY);
  } catch (err) {
    console.log((err as Error).name); // 'EmptyError'
  }

  return { first, last, fallback };
}

go deeper

for a junior

Recall that toPromise() is deprecated in RxJS 7 and that firstValueFrom and lastValueFrom are the replacements, imported from 'rxjs'.

for a middle

Explain which value each function returns, that both reject with EmptyError on an empty source unless given a defaultValue, and that firstValueFrom unsubscribes after one value.

for a senior

Spot the hang: awaiting lastValueFrom on a stream that never completes, or firstValueFrom on a silent one, and bound it with take, timeout or takeUntil before converting.

for a principal

Set the boundary rule for a codebase: convert to Promises only at edges such as async handlers and tests, and keep continuous, cancellable work inside RxJS operators.

## Why toPromise() had to go `Observable.prototype.toPromise()` subscribed to the source and resolved with the **last** value when the source completed. Two problems followed from that design: - **The name hid the choice.** An Observable can emit many values, and "to promise" says nothing about which one the Promise gets. Readers regularly assumed it was the first. - **Empty streams resolved with `undefined`.** A source that completed without emitting made the Promise resolve with `undefined`. RxJS 7 corrected the return type to `Promise<T | undefined>`, which was itself a breaking change for typed code. RxJS 7 therefore **deprecated `toPromise()`** - its JSDoc says it will be removed in v8 - and added two standalone functions exported from `'rxjs'` that make the choice explicit. ## firstValueFrom `firstValueFrom(source$)` subscribes and: 1. on the first `next`, **resolves** with that value and **unsubscribes** from the source, running its teardown; 2. on `error` before any value, **rejects** with that error; 3. on `complete` before any value, **rejects with `EmptyError`** (message `no elements in sequence`), or resolves with the supplied default if a second argument `{ defaultValue }` was passed. Because it unsubscribes after the first value, it is safe for sources that keep emitting, such as an `interval` or a state stream that always has a current value. ## lastValueFrom `lastValueFrom(source$)` subscribes, remembers each value, and settles only on a terminal notification: - on `complete` with at least one value, it **resolves with the last value**; - on `complete` with no value, it **rejects with `EmptyError`** unless `{ defaultValue }` is given; - on `error`, it **rejects** with that error. It is the direct successor of `toPromise()`, with the difference that emptiness is an error rather than a silent `undefined`. ## Side by side | | `toPromise()` (deprecated) | `firstValueFrom` | `lastValueFrom` | |---|---|---|---| | Settles on | completion | first value | completion | | Value | last | first | last | | Empty stream | resolves `undefined` | `EmptyError` or `defaultValue` | `EmptyError` or `defaultValue` | | Unsubscribes early | no | yes, after the first value | no | | Never-completing source | hangs | fine if a value arrives | hangs | ## The endless-stream trap The RxJS documentation warns about this explicitly: a Promise cannot be cancelled, so if the source **neither emits nor completes**, `firstValueFrom` never settles, and `lastValueFrom` never settles for any source that does not complete. The `async` function awaiting it stays suspended, and everything it closes over stays in memory. Typical culprits are a `Subject` nobody calls `next` on and a state stream awaited with `lastValueFrom`. Bound the source before converting: - `take(1)` or `first()` to limit how many values are needed; - `timeout(...)` to turn silence into an error after a deadline; - `takeUntil(...)` to end it when something else happens. ## Typing and a third option Without a config, `firstValueFrom<T>(source$)` returns `Promise<T>`. With `{ defaultValue }` the type widens to `Promise<T | D>`, where `D` is the default's type, so passing `{ defaultValue: null }` makes the result nullable and forces callers to handle it. There is also the instance method `source$.forEach(fn)`, which calls `fn` for every value and returns a `Promise<void>` that resolves on completion and rejects on error; it is for side effects, not for extracting a value. ## When conversion is the right move Converting is appropriate at the edge of RxJS code: a one-shot request inside an `async` function, a test that wants to `await` a result, or an API that must return a Promise. Converting a genuinely continuous stream loses the rest of its values and the ability to cancel, so inside reactive code it is usually better to stay in operators. An Observable cannot be awaited directly; `await source$` just returns the Observable object itself without subscribing. ## Interview checklist A complete answer names the two replacements, states which value each returns, mentions `EmptyError` and the `defaultValue` config, and points out the hang on sources that do not emit or complete. Mentioning that `firstValueFrom` unsubscribes after the first value shows you know it is safe on infinite sources.

  • In RxJS, what happens if you call lastValueFrom on a BehaviorSubject nobody ever completes?
    The Promise never settles. `lastValueFrom` resolves only on completion, and the subject keeps its subscription open indefinitely, so the awaiting `async` function stays suspended and its closure stays in memory. Use `firstValueFrom` to read the current value, or bound the source with `take(1)` or `timeout` before converting.
  • In RxJS 7, how do you make firstValueFrom resolve instead of reject when a source completes without emitting?
    Pass a config object as the second argument: `firstValueFrom(source$, { defaultValue: null })`. When the source completes with no values, the Promise resolves with that default instead of rejecting with `EmptyError`. An error from the source still rejects, and a source that neither emits nor completes still hangs.

saying these in an interview costs you the question

  • toPromise() resolves with the first value the Observable emits.
  • firstValueFrom keeps the source subscribed after the first value arrives.
  • lastValueFrom resolves with undefined when the source completes without emitting.
  • A timeout is built into firstValueFrom, so it cannot hang on a silent source.
  • toPromise() is the recommended conversion in RxJS 7.