rxjs Documentation

A reactive programming library for JavaScript

31,702 stars TypeScript TypeScript 1 file ยท ~15,177 tokens #javascript#rxjs
RAW Doc

1. Project Overview & Quickstart (ReactiveX/rxjs)

File: README.md

RxJS








RxJS is a library for composing asynchronous and event-based programs with
Observable values. This repository contains the platform-based next generation
of RxJS, planned for release as RxJS 9.

RxJS 9 is prerelease work in development. The planned first beta is
9.0.0-beta.0, but it has not been published to npm yet. The next tag still
points to the earlier RxJS 8 prerelease, while RxJS 7 remains the production
latest line and continues to be maintained.

Why RxJS 9? What happened to RxJS 8?

RxJS 8 was real work, not a skipped release. Development began years ago and
was paused while the Web Platform Observable proposal was finalized. The new
implementation is a platform-based generation rather than a continuation of
that paused RxJS 8 branch, so it starts at version 9 to make the architectural
break unmistakable and avoid presenting the old RxJS 8 work as the released
product.

What is different in RxJS 9?

  • RxJS uses the native web-platform Observable when one exists and installs
    a conforming fallback only when needed.
  • RxJS operators and factories are exact, module-owned Symbols. They do not add
    string-named RxJS methods to the platform API.
  • Platform Observable behavior and producer-per-subscription behavior are
    explicit, separate contracts. Use ColdObservable when each direct
    subscription must create its own producer.
  • Cancellation is built on AbortSignal and the platform Subscriber lifecycle.
  • Published JavaScript is ESM-only. Current Node can bridge require() to the
    same ESM files; there is no duplicate CommonJS build.

Preview the RxJS 9 API

The following example shows the planned beta API. Until 9.0.0-beta.0 is
published, do not use npm's next tag to install RxJS 9.

ts
import { ColdObservable } from 'rxjs';
import { map } from 'rxjs/map';

const source = new ColdObservable<number>((subscriber) => {
  subscriber.next(1);
  subscriber.next(2);
  subscriber.complete();
});

source[map]((value) => value * 2).subscribe(console.log);

Import the Symbol and call it with bracket syntax. A platform method such as
observable.map(project) remains the platform contract;
observable[map](project) is the separately versioned RxJS contract.

Packages and documentation

Package Purpose Documentation
rxjs Symbol extensions and intentional RxJS primitives Package guide ยท API ยท RxJS 7 migration
@rxjs/observable-polyfill Conditional platform Observable fallback Package guide
@rxjs/test Implementation-neutral virtual-time and marble testing Package guide
@rxjs/migrate Deterministic migration engine and canonical agent Skill Package guide

Release support, budgets, and exact environment gates are documented in the
rxjs package. The irreversible npm
publication process is the public secure release runbook.
The RxJS 9 security-assurance document
explains the release evidence, verification commands, sole-maintainer model,
and OpenSSF Scorecard in context.
Repository-wide design records live in
docs/rxjs-next.

Supported environments

The planned beta supports Node 22.13+ and Node 24 as blocking lanes, with Node
26 in an advisory lane. Current Chrome, Firefox, desktop Safari, Mobile Safari,
Deno, Bun, and Webpack 5 are blocking. Every supported consumer receives the
same ESM implementation, so Deno and Bun support adds no runtime-specific
package or application-bundle code.

Contributing

Requires Node 22.13+ and pnpm 10.34.5. Run commands from the repository root.

sh
pnpm install
pnpm --filter rxjs exec vitest --run src
pnpm --filter rxjs run test:package
pnpm run release:check

Start with the repository contribution guide and the
rxjs package contribution guide. The active
execution queue is PROJECT_PLAN.md.

The complete source-pinned RxJS 7 corpus intentionally retains reviewed
lifecycle and compatibility divergences, so it is migration evidence rather
than a blanket RxJS 9 compatibility gate. Focused source, package, runtime,
browser, performance, and WPT commands are the release gates.

Community and governance


File: apps/rxjs.dev/content/deprecations/array-argument.md

Array Arguments

To unify the API surface of forkJoin and combineLatest we deprecated some signatures.
Since that it is recommended to either pass an Object or an Array to these operators.

text
This deprecation was introduced in RxJS 6.5.

Operators affected by this Change

How to Refactor

We deprecated the signatures, where just pass all Observables directly as parameters to these operators.

ts
import {forkJoin, from} from 'rxjs';

const odd$ = from([1,3,5]);
const even$ = from([2,4,6]);

// deprecated
forkJoin(odd$, even$);
// suggested change
forkJoin([odd$, even$]);
// or
forkJoin({odd: odd$, even: even$})

File: apps/rxjs.dev/content/deprecations/breaking-changes.md

Breaking Changes in Version 7

General

  • TS: RxJS requires TS 4.2

  • rxjs-compat: rxjs-compat is not published for v7

  • toPromise: toPromise return type now returns T | undefined in TypeScript, which is correct, but may break builds.

  • Subscription: add no longer returns an unnecessary Subscription reference. This was done to prevent confusion caused by a legacy behavior. You can now add and remove functions and Subscriptions as teardowns to and from a Subscription using add and remove directly. Before this, remove only accepted subscriptions.

  • Observable: lift no longer exposed. It was NEVER documented that end users of the library should be creating operators using lift. Lift has a variety of issues and was always an internal implementation detail of rxjs that might have been used by a few power users in the early days when it had the most value. The value of lift, originally, was that subclassed Observables would compose through all operators that implemented lift. The reality is that feature is not widely known, used, or supported, and it was never documented as it was very experimental when it was first added. Until the end of v7, lift will remain on Observable. Standard JavaScript users will notice no difference. However, TypeScript users might see complaints about lift not being a member of observable. To workaround this issue there are two things you can do: 1. Rewrite your operators as outlined in the documentation, such that they return new Observable. or 2. cast your observable as any and access lift that way. Method 1 is recommended if you do not want things to break when we move to version 8.

  • Subscriber: new Subscriber no longer takes 0-3 arguments. To create a Subscriber with 0-3 arguments, use Subscriber.create. However, please note that there is little to no reason that you should be creating Subscriber references directly, and Subscriber.create and new Subscriber are both deprecated.

  • onUnhandledError: Errors that occur during setup of an observable subscription after the subscription has emitted an error or completed will now throw in their own call stack. Before it would call console.warn. This is potentially breaking in edge cases for node applications, which may be configured to terminate for unhandled exceptions. In the unlikely event this affects you, you can configure the behavior to console.warn in the new configuration setting like so: import { config } from 'rxjs'; config.onUnhandledError = (err) => console.warn(err);

  • RxJS Error types Tests that are written with naive expectations against errors may fail now that errors have a proper stack property. In some testing frameworks, a deep equality check on two error instances will check the values in stack, which could be different.

  • unsubscribe no longer available via the this context of observer functions. To reenable, set config.useDeprecatedNextContext = true on the rxjs config found at import { config } from 'rxjs';. Note that enabling this will result in a performance penalty for all consumer subscriptions.

  • Leaked implementation detail _unsubscribeAndRecycle of Subscriber has been removed. Just use new Subscription objects

  • The static sortActions method on VirtualTimeScheduler is no longer publicly exposed by our TS types.

  • Notification.createNext(undefined) will no longer return the exact same reference every time.

  • Type signatures tightened up around Notification and dematerialize, may uncover issues with invalid types passed to those operators.

  • Experimental support for for await has been removed. Use https://github.com/benlesh/rxjs-for-await instead.

  • ReplaySubject no longer schedules emissions when a scheduler is provided. If you need that behavior,
    please compose in observeOn using pipe, for example: new ReplaySubject(2, 3000).pipe(observeOn(asap))

  • rxjs-compat: rxjs/Rx is no longer a valid import site.

Operators

concat

  • concat: Generic signature changed. Recommend not explicitly passing generics, just let inference do its job. If you must, cast with as.
  • of: Generic signature changed, do not specify generics, allow them to be inferred or use as

count

  • count: No longer passes source observable as a third argument to the predicate. That feature was rarely used, and of limited value. The workaround is to simply close over the source inside of the function if you need to access it in there.

defer

  • defer no longer allows factories to return void or undefined. All factories passed to defer must return a proper ObservableInput, such as Observable, Promise, et al. To get the same behavior as you may have relied on previously, return EMPTY or return of() from the factory.

map

  • map: thisArg will now default to undefined. The previous default of MapSubscriber never made any sense. This will only affect code that calls map with a function and references this like so: source.pipe(map(function () { console.log(this); })). There wasn't anything useful about doing this, so the breakage is expected to be very minimal. If anything we're no longer leaking an implementation detail.

mergeScan

  • mergeScan: mergeScan will no longer emit its inner state again upon completion.

of

  • of: Use with more than 9 arguments, where the last argument is a SchedulerLike may result in the wrong type which includes the SchedulerLike, even though the run time implementation does not support that. Developers should be using scheduled instead

pairs

  • pairs: pairs will no longer function in IE without a polyfill for Object.entries. pairs itself is also deprecated in favor of users just using from(Object.entries(obj)).

race

  • race: race() will no longer subscribe to subsequent observables if a provided source synchronously errors or completes. This means side effects that might have occurred during subscription in those rare cases will no longer occur.

repeat

  • An undocumented behavior where passing a negative count argument to repeat would result in an observable that repeats forever.

retry

  • Removed an undocumented behavior where passing a negative count argument to retry would result in an observable that repeats forever.

single

  • single operator will now throw for scenarios where values coming in are either not present, or do not match the provided predicate. Error types have thrown have also been updated, please check documentation for changes.

skipLast

  • skipLast: skipLast will no longer error when passed a negative number, rather it will simply return the source, as though 0 was passed.

startWith

  • startWith: startWith will return incorrect types when called with more than 7 arguments and a scheduler. Passing scheduler to startWith is deprecated

take

  • take and will now throw runtime error for arguments that are negative or NaN, this includes non-TS calls like take().

takeLast

  • takeLast now has runtime assertions that throw TypeErrors for invalid arguments. Calling takeLast without arguments or with an argument that is NaN will throw a TypeError

throwError

  • throwError: In an extreme corner case for usage, throwError is no longer able to emit a function as an error directly. If you need to push a function as an error, you will have to use the factory function to return the function like so: throwError(() => functionToEmit), in other words throwError(() => () => console.log('called later')).

timestamp

  • timestamp operator accepts a TimestampProvider, which is any object with a now method
    that returns a number. This means pulling in less code for the use of the timestamp operator. This may cause
    issues with TestScheduler run mode. (see Issue here)

zip

  • zip: Zipping a single array will now have a different result. This is an extreme corner-case, because it is very unlikely that anyone would want to zip an array with nothing at all. The workaround would be to wrap the array in another array zip([[1,2,3]]). But again, that's pretty weird.

  • zip: zip operators will no longer iterate provided iterables "as needed", instead the iterables will be treated as push-streams just like they would be everywhere else in RxJS. This means that passing an endless iterable will result in the thread locking up, as it will endlessly try to read from that iterable. This puts us in-line with all other Rx implementations. To work around this, it is probably best to use map or some combination of map and zip. For example, zip(source$, iterator) could be source$.pipe(map(value => [value, iterator.next().value])).

ajax

  • ajax body serialization will now use default XHR behavior in all cases. If the body is a Blob, ArrayBuffer, any array buffer view (like a byte sequence, e.g. Uint8Array, etc), FormData, URLSearchParams, string, or ReadableStream, default handling is use. If the body is otherwise typeof "object", then it will be converted to JSON via JSON.stringify, and the Content-Type header will be set to application/json;charset=utf-8. All other types will emit an error.

  • The Content-Type header passed to ajax configuration no longer has any effect on the serialization behavior of the AJAX request.

  • For TypeScript users, AjaxRequest is no longer the type that should be explicitly used to create an ajax. It is now AjaxConfig, although the two types are compatible, only AjaxConfig has progressSubscriber and createXHR.

  • ajax: In an extreme corner-case... If an error occurs, the responseType is "json", we're in IE, and the responseType is not valid JSON, the ajax observable will no longer emit a syntax error, rather it will emit a full AjaxError with more details.

  • ajax: Ajax implementation drops support for IE10 and lower. This puts us in-line with other implementations and helps clean up code in this area


File: apps/rxjs.dev/content/deprecations/index.md

Deprecations and Breaking Changes

While the core team always tries to limit changes, sometimes we have to deprecate APIs or do breaking changes for various reasons.
This section aims to describe some of the deprecations and breaking changes we did more in detail. Some of the changes are to extensive to describe
them appropriately in a changelog. Additionally, we can provide code examples in the documentation, to make required changes more comprehensible and therefore
lower migration efforts.

Do notice that this is not a complete list, please see the changelog for the complete list.


File: apps/rxjs.dev/content/deprecations/multicasting.md

Multicasting

In version 7, the multicasting APIs were simplified to just a few functions:

And shareReplay - which is a thin wrapper around the now highly-configurable share operator.

Other APIs that relate to multicasting are now deprecated.

text
These deprecations were introduced in RxJS 7.0 and will become breaking in RxJS 8.

APIs affected by this Change

How to refactor

ConnectableObservable

Instead of creating a ConnectableObservable instance, call the connectable function to obtain a connectable observable.

ts
import { ConnectableObservable, timer, Subject } from 'rxjs';

// deprecated
const tick$ = new ConnectableObservable(
  timer(1_000),
  () => new Subject());
tick$.connect();
ts
import { connectable, timer, Subject } from 'rxjs';

// suggested refactor
const tick$ = connectable(timer(1_000), {
  connector: () => new Subject()
});
tick$.connect();

In situations in which the refCount method is used, the share operator can be used instead.

ts
import { ConnectableObservable, timer, Subject } from 'rxjs';

// deprecated
const tick$ = new ConnectableObservable(
  timer(1_000),
  () => new Subject()
).refCount();
ts
import { timer, share, Subject } from 'rxjs';

// suggested refactor
const tick$ = timer(1_000).pipe(
  share({ connector: () => new Subject() })
);

multicast

Where multicast is called with a subject factory, can be replaced with connectable.

ts
import { timer, multicast, Subject, ConnectableObservable } from 'rxjs';

// deprecated
const tick$ = timer(1_000).pipe(
  multicast(() => new Subject())
) as ConnectableObservable<number>;
ts
import { connectable, timer, Subject } from 'rxjs';

// suggested refactor
const tick$ = connectable(timer(1_000), {
  connector: () => new Subject()
});

Where multicast is called with a subject instance, it can be replaced with connectable and a local subject instance.

ts
import { timer, multicast, Subject, ConnectableObservable } from 'rxjs';

// deprecated
const tick$ = timer(1_000).pipe(
  multicast(new Subject())
) as ConnectableObservable<number>;
ts
import { connectable, timer, Subject } from 'rxjs';

// suggested refactor
const tick$ = connectable(timer(1_000), {
  connector: () => new Subject(),
  resetOnDisconnect: false
});

Where multicast is used in conjunction with refCount, it can be replaced with share.

ts
import { timer, multicast, Subject, refCount } from 'rxjs';

// deprecated
const tick$ = timer(1_000).pipe(
  multicast(() => new Subject()),
  refCount()
);
ts
import { timer, share, Subject } from 'rxjs';

// suggested refactor
const tick$ = timer(1_000).pipe(
  share({ connector: () => new Subject() })
);

Where multicast is used with a selector, it can be replaced with connect.

ts
import { timer, multicast, Subject, combineLatest } from 'rxjs';

// deprecated
const tick$ = timer(1_000).pipe(
  multicast(
    () => new Subject(),
    (source) => combineLatest([source, source])
  )
);
ts
import { timer, connect, combineLatest, Subject } from 'rxjs';

// suggested refactor
const tick$ = timer(1_000).pipe(
  connect((source) => combineLatest([source, source]), {
    connector: () => new Subject()
  })
);

publish

If you're using publish to create a ConnectableObservable, you can use connectable instead.

ts
import { timer, publish, ConnectableObservable } from 'rxjs';

// deprecated
const tick$ = timer(1_000).pipe(
  publish()
) as ConnectableObservable<number>;
ts
import { connectable, timer, Subject } from 'rxjs';

// suggested refactor
const tick$ = connectable(timer(1_000), {
  connector: () => new Subject<number>(),
  resetOnDisconnect: false
});

And if refCount is being applied to the result of publish, you can use share to replace both.

ts
import { timer, publish, refCount } from 'rxjs';

// deprecated
const tick$ = timer(1_000).pipe(
  publish(),
  refCount()
);
ts
import { timer, share } from 'rxjs';

// suggested refactor
const tick$ = timer(1_000).pipe(
  share({
    resetOnError: false,
    resetOnComplete: false,
    resetOnRefCountZero: false
  })
);

If publish is being called with a selector, you can use the connect operator instead.

ts
import { timer, publish, combineLatest } from 'rxjs';

// deprecated
const tick$ = timer(1_000).pipe(
  publish((source) => combineLatest([source, source]))
);
ts
import { timer, connect, combineLatest } from 'rxjs';

// suggested refactor
const tick$ = timer(1_000).pipe(
  connect((source) => combineLatest([source, source]))
);

publishBehavior

If you're using publishBehavior to create a ConnectableObservable, you can use connectable and a BehaviorSubject instead.

ts
import { timer, publishBehavior, ConnectableObservable } from 'rxjs';

// deprecated
const tick$ = timer(1_000).pipe(
  publishBehavior(0)
) as ConnectableObservable<number>;
ts
import { connectable, timer, BehaviorSubject } from 'rxjs';

// suggested refactor
const tick$ = connectable(timer(1_000), {
  connector: () => new BehaviorSubject(0),
  resetOnDisconnect: false
});

And if refCount is being applied to the result of publishBehavior, you can use the share operator - with a BehaviorSubject connector - to replace both.

ts
import { timer, publishBehavior, refCount } from 'rxjs';

// deprecated
const tick$ = timer(1_000).pipe(
  publishBehavior(0),
  refCount()
);
ts
import { timer, share, BehaviorSubject } from 'rxjs';

// suggested refactor
const tick$ = timer(1_000).pipe(
  share({
    connector: () => new BehaviorSubject(0),
    resetOnError: false,
    resetOnComplete: false,
    resetOnRefCountZero: false
  })
);

publishLast

If you're using publishLast to create a ConnectableObservable, you can use connectable and an AsyncSubject instead.

ts
import { timer, publishLast, ConnectableObservable } from 'rxjs';

// deprecated
const tick$ = timer(1_000).pipe(
  publishLast()
) as ConnectableObservable<number>;
ts
import { connectable, timer, AsyncSubject } from 'rxjs';

// suggested refactor
const tick$ = connectable(timer(1_000), {
  connector: () => new AsyncSubject<number>(),
  resetOnDisconnect: false
});

And if refCount is being applied to the result of publishLast, you can use the share operator - with an AsyncSubject connector - to replace both.

ts
import { timer, publishLast, refCount } from 'rxjs';

// deprecated
const tick$ = timer(1_000).pipe(
  publishLast(),
  refCount()
);
ts
import { timer, share, AsyncSubject } from 'rxjs';

// suggested refactor
const tick$ = timer(1_000).pipe(
  share({
    connector: () => new AsyncSubject(),
    resetOnError: false,
    resetOnComplete: false,
    resetOnRefCountZero: false
  })
);

publishReplay

If you're using publishReplay to create a ConnectableObservable, you can use connectable and a ReplaySubject instead.

ts
import { timer, publishReplay, ConnectableObservable } from 'rxjs';

// deprecated
const tick$ = timer(1_000).pipe(
  publishReplay(1)
) as ConnectableObservable<number>;
ts
import { connectable, timer, ReplaySubject } from 'rxjs';

// suggested refactor
const tick$ = connectable(timer(1_000), {
  connector: () => new ReplaySubject<number>(1),
  resetOnDisconnect: false
});

And if refCount is being applied to the result of publishReplay, you can use the share operator - with a ReplaySubject connector - to replace both.

ts
import { timer, publishReplay, refCount } from 'rxjs';

// deprecated
const tick$ = timer(1_000).pipe(
  publishReplay(1),
  refCount()
);
ts
import { timer, share, ReplaySubject } from 'rxjs';

// suggested refactor
const tick$ = timer(1_000).pipe(
  share({
    connector: () => new ReplaySubject(1),
    resetOnError: false,
    resetOnComplete: false,
    resetOnRefCountZero: false
  })
);

If publishReplay is being called with a selector, you can use the connect operator - with a ReplaySubject connector - instead.

ts
import { timer, publishReplay, combineLatest } from 'rxjs';

// deprecated
const tick$ = timer(1_000).pipe(
  publishReplay(1, undefined, (source) => combineLatest([source, source]))
);
ts
import { timer, connect, combineLatest, ReplaySubject } from 'rxjs';

// suggested refactor
const tick$ = timer(1_000).pipe(
  connect((source) => combineLatest([source, source]), {
    connector: () => new ReplaySubject(1)
  })
);

refCount

Instead of applying the refCount operator to the ConnectableObservable obtained from a multicast
or publish operator, use the share operator to replace both.

The properties passed to share will depend upon the operators that are being replaced. The refactors for using refCount with multicast, publish, publishBehavior, publishLast and publishReplay are detailed above.


File: apps/rxjs.dev/content/deprecations/resultSelector.md

ResultSelector Parameter

Some operator supported a resultSelector argument that acted as mapping function on the result of that operator.
The same behavior can be reproduced with the map operator, therefore this argument became deprecated.

text
This deprecation was introduced in RxJS 6.0 and will become breaking with RxJS 8.

There were two reasons for actually deprecating those parameters:

  1. It increases the bundle size of every operator
  2. In some scenarios values had to be retained in memory causing a general memory pressure

Operators affected by this Change

How to Refactor

Instead of using the resultSelector Argument, you can leverage the map operator on the inner Observable:

ts
import { fromEvent, switchMap, interval, map } from 'rxjs';

// deprecated
fromEvent(document, 'click').pipe(
  switchMap((x) => interval(1000), (_, x) => x + 1)
);
// suggested change
fromEvent(document, 'click').pipe(
  switchMap((x) => interval(1000).pipe(map((x) => x + 1)))
);

File: apps/rxjs.dev/content/deprecations/scheduler-argument.md

Scheduler Argument

To limit the API surface of some operators, but also prepare for a major refactoring in V8, we
agreed on deprecating the scheduler argument from many operators. It solely deprecates those methods where this argument is rarely used. So time related
operators, like interval are not affected by this deprecation.

To support this transition the scheduled creation function was added.

text
This deprecation was introduced in RxJS 6.5 and will become breaking with RxJS 8.

Operators affected by this Change

How to Refactor

If you use any operator from the above list and you're passing the scheduler argument, you have three potential refactoring options.

Refactoring of `of` and `from`

scheduled is kinda copying the behavior of from. Therefore if you used from with a scheduler argument, you can just replace them.

For the of creation function you need to replace this Observable with scheduled and instead of passing the scheduler argument to of pass it to scheduled.
Following code example demonstrate this process.

ts
import { of, asyncScheduler, scheduled } from 'rxjs';

// Deprecated approach
of(1, 2, 3, asyncScheduler).subscribe((x) => console.log(x));
// suggested approach
scheduled([1, 2, 3], asyncScheduler).subscribe((x) => console.log(x));

Refactoring of `merge`, `concat`, `combineLatest`, `startWith` and `endWith`

In case you used to pass a scheduler argument to one of these operators you probably had code like this:

ts
import { concat, of, asyncScheduler } from 'rxjs';

concat(of('hello '), of('World'), asyncScheduler).subscribe((x) => console.log(x));

To work around this deprecation you can leverage the scheduled function.

ts
import { scheduled, of, asyncScheduler, concatAll } from 'rxjs';

scheduled([of('hello '), of('World')], asyncScheduler)
  .pipe(concatAll())
  .subscribe((x) => console.log(x));

You can apply this pattern to refactor deprecated usage of concat, startWith and endWith but do notice that you will want to use mergeAll to refactor the deprecated usage of merge.

With combineLatest, you will want to use combineLatestAll

E.g. code that used to look like this:

ts
import { combineLatest, of, asyncScheduler } from 'rxjs';

combineLatest(of('hello '), of('World'), asyncScheduler).subscribe(console.log);

would become:

ts
import { scheduled, of, asyncScheduler, combineLatestAll } from 'rxjs';

scheduled([of('hello '), of('World')], asyncScheduler)
  .pipe(combineLatestAll())
  .subscribe((x) => console.log(x));

File: apps/rxjs.dev/content/deprecations/subscribe-arguments.md

Subscribe Arguments

You might have seen that we deprecated some signatures of the subscribe method, which might have caused some confusion.
The subscribe method itself is not deprecated. This deprecation also affects the tap operator, as tap supports the same signature as the subscribe method.

This is to get ready for a future where we may allow configuration of subscribe via the second argument, for things like AbortSignal or the like (imagine source$.subscribe(fn, { signal }), etc). This deprecation is also because 2-3 function arguments can contribute to harder-to-read code. For example someone could name functions poorly and confuse the next reader: source$.subscribe(doSomething, doSomethingElse, lol) With that signature, you have to know unapparent details about subscribe, where using a partial observer solves that neatly: source$.subscribe({ next: doSomething, error: doSomethingElse, complete: lol }).

text
This deprecation was introduced in RxJS 6.4.

In short we deprecated all signatures where you specified an anonymous error or complete callback and passed an empty function to one of the callbacks before.

What Signature is affected

We have deprecated all signatures of subscribe that take more than 1 argument.

We deprecated signatures for just passing the complete callback.

ts
import { of } from 'rxjs';

// deprecated
of([1,2,3]).subscribe(null, null, console.info); // difficult to read
// suggested change
of([1,2,3]).subscribe({complete: console.info});

Similarly, we also deprecated signatures for solely passing the error callback.

ts
import { throwError } from 'rxjs';

// deprecated 
throwError('I am an error').subscribe(null, console.error);
// suggested change
throwError('I am an error').subscribe({error: console.error});

Do notice, in general it is recommended only to use the anonymous function if you only specify the next callback otherwise
we recommend to pass an Observer

ts
import { of } from 'rxjs';

// recommended 
of([1,2,3]).subscribe((v) => console.info(v));
// also recommended
of([1,2,3]).subscribe({
    next: (v) => console.log(v),
    error: (e) => console.error(e),
    complete: () => console.info('complete') 
})

File: apps/rxjs.dev/content/deprecations/to-promise.md

Conversion to Promises

The similarity between Observables and Promises is that both collections may produce values over
time, but the difference is that Observables may produce none or more than one value, while Promises produce only one
value when resolved successfully.

Issues

For this reason, in RxJS 7, the return type of the Observable's toPromise()
method has been fixed to better reflect the fact that Observables can yield zero values. This may be a breaking
change
to some projects as the return type was changed from Promise<T> to Promise<T | undefined>.

Also, toPromise() method name was never indicating what emitted value a Promise will resolve with because Observables
can produce multiple values over time. When converting to a Promise, you might want to choose which value to pick -
either the first value that has arrived or the last one. To fix all these issues, we decided to deprecate toPromise(),
and to introduce the two new helper functions for conversion to Promises.

Use one of the two new functions

As a replacement to the deprecated toPromise() method, you should use one of the two built in static conversion
functions {@link firstValueFrom} or {@link lastValueFrom}.

`lastValueFrom`

The lastValueFrom is almost exactly the same as toPromise() meaning that it will resolve with the last value that has
arrived when the Observable completes, but with the difference in behavior when Observable completes without emitting a
single value. When Observable completes without emitting, toPromise() will successfully resolve with undefined (thus
the return type change), while the lastValueFrom will reject with the {@link EmptyError}. Thus, the return type of the
lastValueFrom is Promise<T>, just like toPromise() had in RxJS 6.

Example

ts
import { interval, take, lastValueFrom } from 'rxjs';

async function execute() {
  const source$ = interval(2000).pipe(take(10));
  const finalNumber = await lastValueFrom(source$);
  console.log(`The final number is ${finalNumber}`);
}

execute();

// Expected output:
// "The final number is 9"

`firstValueFrom`

However, you might want to take the first value as it arrives without waiting an Observable to complete, thus you can
use firstValueFrom. The firstValueFrom will resolve a Promise with the first value that was emitted from the
Observable and will immediately unsubscribe to retain resources. The firstValueFrom will also reject with an
{@link EmptyError} if the Observable completes with no values emitted.

Example

ts
import { interval, firstValueFrom } from 'rxjs';

async function execute() {
  const source$ = interval(2000);
  const firstNumber = await firstValueFrom(source$);
  console.log(`The first number is ${firstNumber}`);
}

execute();

// Expected output:
// "The first number is 0"

Both functions will return a Promise that rejects if the source Observable errors. The Promise
will reject with the same error that the Observable has errored with.

Use default value

If you don't want Promises created by lastValueFrom or firstValueFrom to reject with {@link EmptyError} if there
were no emissions before completion, you can use the second parameter. The second parameter is expected to be an object
with defaultValue parameter. The value in the defaultValue will be used to resolve a Promise when source Observable
completes without emitted values.

ts
import { firstValueFrom, EMPTY } from 'rxjs';

const result = await firstValueFrom(EMPTY, { defaultValue: 0 });
console.log(result);

// Expected output:
// 0

Warning

Only use lastValueFrom function if you know an Observable will eventually complete. The firstValueFrom function should
be used if you know an Observable will emit at least one value or will eventually complete. If the source Observable
does not complete or emit, you will end up with a Promise that is hung up, and potentially all of the state of an async
function hanging out in memory. To avoid this situation, look into adding something like {@link timeout}, {@link take},
{@link takeWhile}, or {@link takeUntil} amongst others.


File: apps/rxjs.dev/content/guide/testing/marble-testing.md

Testing RxJS Code with Marble Diagrams

This guide refers to usage of marble diagrams when using the new testScheduler.run(callback). Some details here do not apply to using the TestScheduler manually, without using the run() helper.

We can test our asynchronous RxJS code synchronously and deterministically by virtualizing time using the TestScheduler. Marble diagrams provide a visual way for us to represent the behavior of an Observable. We can use them to assert that a particular Observable behaves as expected, as well as to create hot and cold Observables we can use as mocks.

At this time, the TestScheduler can only be used to test code that uses RxJS schedulers - AsyncScheduler, etc. If the code consumes a Promise, for example, it cannot be reliably tested with TestScheduler, but instead should be tested more traditionally. See the Known Issues section for more details.

ts
import { TestScheduler } from 'rxjs/testing';
import { throttleTime } from 'rxjs';

const testScheduler = new TestScheduler((actual, expected) => {
  // asserting the two objects are equal - required
  // for TestScheduler assertions to work via your test framework
  // e.g. using chai.
  expect(actual).deep.equal(expected);
});

// This test runs synchronously.
it('generates the stream correctly', () => {
  testScheduler.run((helpers) => {
    const { cold, time, expectObservable, expectSubscriptions } = helpers;
    const e1 = cold(' -a--b--c---|');
    const e1subs = '  ^----------!';
    const t = time('   ---|       '); // t = 3
    const expected = '-a-----c---|';

    expectObservable(e1.pipe(throttleTime(t))).toBe(expected);
    expectSubscriptions(e1.subscriptions).toBe(e1subs);
  });
});

API

The callback function you provide to testScheduler.run(callback) is called with helpers object that contains functions you'll use to write your tests.

text
When the code inside this callback is being executed, any operator that uses timers/AsyncScheduler (like delay, debounceTime, etc.,) will automatically use the TestScheduler instead, so that we have "virtual time". You do not need to pass the TestScheduler to them, like in the past.
ts
testScheduler.run((helpers) => {
  const { cold, hot, expectObservable, expectSubscriptions, flush, time, animate } = helpers;
  // use them
});

Although run() executes entirely synchronously, the helper functions inside your callback function do not! These functions schedule assertions that will execute either when your callback completes or when you explicitly call flush(). Be wary of calling synchronous assertions, for example expect, from your testing library of choice, from within the callback. See Synchronous Assertion for more information on how to do this.

  • cold(marbleDiagram: string, values?: object, error?: any) - creates a "cold" observable whose subscription starts when the test begins.

  • hot(marbleDiagram: string, values?: object, error?: any) - creates a "hot" observable (like a subject) that will behave as though it's already "running" when the test begins. An interesting difference is that hot marbles allow a ^ character to signal where the "zero frame" is. That is the point at which the subscription to observables being tested begins.

  • expectObservable(actual: Observable<T>, subscriptionMarbles?: string).toBe(marbleDiagram: string, values?: object, error?: any) - schedules an assertion for when the TestScheduler flushes. Give subscriptionMarbles as parameter to change the schedule of subscription and unsubscription. If you don't provide the subscriptionMarbles parameter it will subscribe at the beginning and never unsubscribe. Read below about subscription marble diagram.

  • expectSubscriptions(actualSubscriptionLogs: SubscriptionLog[]).toBe(subscriptionMarbles: string) - like expectObservable schedules an assertion for when the testScheduler flushes. Both cold() and hot() return an observable with a property subscriptions of type SubscriptionLog[]. Give subscriptions as parameter to expectSubscriptions to assert whether it matches the subscriptionsMarbles marble diagram given in toBe(). Subscription marble diagrams are slightly different than Observable marble diagrams. Read more below.

  • flush() - immediately starts virtual time. Not often used since run() will automatically flush for you when your callback returns, but in some cases you may wish to flush more than once or otherwise have more control.

  • time() - converts marbles into a number indicating number of frames. It can be used by operators expecting a specific timeout. It measures time based on the position of the complete (|) signal:

    ts
    testScheduler.run((helpers) => {
      const { time, cold } = helpers;
      const source = cold('---a--b--|');
      const t = time('        --|    ');
      //                         --|
      const expected = '   -----a--b|';
      const result = source.pipe(delay(t));
      expectObservable(result).toBe(expected);
    });
  • animate() - specifies when requested animation frames will be 'painted'. animate accepts a marble diagram and each value emission in the diagram indicates when a 'paint' occurs - at which time, any queued requestAnimationFrame callbacks will be executed. Call animate at the beginning of your test and align the marble diagrams so that it's clear when the callbacks will be executed:

    ts
    testScheduler.run((helpers) => {
      const { animate, cold } = helpers;
      animate('              ---x---x---x---x');
      const requests = cold('-r-------r------');
      /* ... */
      const expected = '     ---a-------b----';
    });

Marble syntax

In the context of TestScheduler, a marble diagram is a string containing special syntax representing events happening over virtual time. Time progresses by frames. The first character of any marble string always represents the zero frame, or the start of time. Inside of testScheduler.run(callback) the frameTimeFactor is set to 1, which means one frame is equal to one virtual millisecond.

How many virtual milliseconds one frame represents depends on the value of TestScheduler.frameTimeFactor. For legacy reasons the value of frameTimeFactor is 1 only when your code inside the testScheduler.run(callback) callback is running. Outside of it, it's set to 10. This will likely change in a future version of RxJS so that it is always 1.

IMPORTANT: This syntax guide refers to usage of marble diagrams when using the new testScheduler.run(callback). The semantics of marble diagrams when using the TestScheduler manually are different, and some features like the new time progression syntax are not supported.

  • ' ' whitespace: horizontal whitespace is ignored, and can be used to help vertically align multiple marble diagrams.

  • '-' frame: 1 "frame" of virtual time passing (see above description of frames).

  • [0-9]+[ms|s|m] time progression: the time progression syntax lets you progress virtual time by a specific amount. It's a number, followed by a time unit of ms (milliseconds), s (seconds), or m (minutes) without any space between them, e.g. a 10ms b. See Time progression syntax for more details.

  • '|' complete: The successful completion of an observable. This is the observable producer signaling complete().

  • '#' error: An error terminating the observable. This is the observable producer signaling error().

  • [a-z0-9] e.g. 'a' any alphanumeric character: Represents a value being emitted by the producer signaling next(). Also consider that you could map this into an object or an array like this:

    ts
    const expected = '400ms (a-b|)';
    const values = {
      a: 'value emitted',
      b: 'another value emitted',
    };
    
    expectObservable(someStreamForTesting).toBe(expected, values);
    
    // This would work also
    const expected = '400ms (0-1|)';
    const values = [
      'value emitted',
      'another value emitted'
    ];
    
    expectObservable(someStreamForTesting).toBe(expected, values);
  • '()' sync groupings: When multiple events need to be in the same frame synchronously, parentheses are used to group those events. You can group next'd values, a completion, or an error in this manner. The position of the initial ( determines the time at which its values are emitted. While it can be counter-intuitive at first, after all the values have synchronously emitted time will progress a number of frames equal to the number of ASCII characters in the group, including the parentheses. e.g. '(abc)' will emit the values of a, b, and c synchronously in the same frame and then advance virtual time by 5 frames, '(abc)'.length === 5. This is done because it often helps you vertically align your marble diagrams, but it's a known pain point in real-world testing. Learn more about known issues.

  • '^' subscription point: (hot observables only) shows the point at which the tested observables will be subscribed to the hot observable. This is the "zero frame" for that observable, every frame before the ^ will be negative. Negative time might seem pointless, but there are in fact advanced cases where this is necessary, usually involving ReplaySubjects.

Time progression syntax

The new time progression syntax takes inspiration from the CSS duration syntax. It's a number (integer or floating point) immediately followed by a unit; ms (milliseconds), s (seconds), m (minutes). e.g. 100ms, 1.4s, 5.25m.

When it's not the first character of the diagram it must be padded a space before/after to disambiguate it from a series of marbles. e.g. a 1ms b needs the spaces because a1msb will be interpreted as ['a', '1', 'm', 's', 'b'] where each of these characters is a value that will be next()'d as-is.

NOTE: You may have to subtract 1 millisecond from the time you want to progress because the alphanumeric marbles (representing an actual emitted value) advance time 1 virtual frame themselves already, after they emit. This can be counter-intuitive and frustrating, but for now it is indeed correct.

ts
const input = ' -a-b-c|';
const expected = '-- 9ms a 9ms b 9ms (c|)';

// Depending on your personal preferences you could also
// use frame dashes to keep vertical alignment with the input.
// const input = ' -a-b-c|';
// const expected = '------- 4ms a 9ms b 9ms (c|)';
// or
// const expected = '-----------a 9ms b 9ms (c|)';

const result = cold(input).pipe(
  concatMap((d) => of(d).pipe(
    delay(10)
  ))
);

expectObservable(result).toBe(expected);

Examples

'-' or '------': Equivalent to {@link NEVER}, or an observable that never emits or errors or completes.

|: Equivalent to {@link EMPTY}, or an observable that never emits and completes immediately.

#: Equivalent to {@link throwError}, or an observable that never emits and errors immediately.

'--a--': An observable that waits 2 "frames", emits value a on frame 2 and then never completes.

'--a--b--|': On frame 2 emit a, on frame 5 emit b, and on frame 8, complete.

'--a--b--#': On frame 2 emit a, on frame 5 emit b, and on frame 8, error.

'-a-^-b--|': In a hot observable, on frame -2 emit a, then on frame 2 emit b, and on frame 5, complete.

'--(abc)-|': on frame 2 emit a, b, and c, then on frame 8, complete.

'-----(a|)': on frame 5 emit a and complete.

'a 9ms b 9s c|': on frame 0 emit a, on frame 10 emit b, on frame 9,011 emit c, then on frame 9,012 complete.

'--a 2.5m b': on frame 2 emit a, on frame 150,003 emit b and never complete.

Subscription marbles

The expectSubscriptions helper allows you to assert that a cold() or hot() Observable you created was subscribed/unsubscribed to at the correct point in time. The subscriptionMarbles parameter to expectObservable allows your test to defer subscription to a later virtual time, and/or unsubscribe even if the observable being tested has not yet completed.

The subscription marble syntax is slightly different to conventional marble syntax.

  • '-' time: 1 frame time passing.
  • [0-9]+[ms|s|m] time progression: the time progression syntax lets you progress virtual time by a specific amount. It's a number, followed by a time unit of ms (milliseconds), s (seconds), or m (minutes) without any space between them, e.g. a 10ms b. See Time progression syntax for more details.
  • '^' subscription point: shows the point in time at which a subscription happens.
  • '!' unsubscription point: shows the point in time at which a subscription is unsubscribed.

There should be at most one ^ point in a subscription marble diagram, and at most one ! point. Other than that, the - character is the only one allowed in a subscription marble diagram.

Examples

'-' or '------': no subscription ever happened.

'--^--': a subscription happened after 2 "frames" of time passed, and the subscription was not unsubscribed.

'--^--!-': on frame 2 a subscription happened, and on frame 5 was unsubscribed.

'500ms ^ 1s !': on frame 500 a subscription happened, and on frame 1,501 was unsubscribed.

Given a hot source, test multiple subscribers that subscribe at different times:

ts
testScheduler.run(({ hot, expectObservable }) => {
  const source = hot('--a--a--a--a--a--a--a--');
  const sub1 = '      --^-----------!';
  const sub2 = '      ---------^--------!';
  const expect1 = '   --a--a--a--a--';
  const expect2 = '   -----------a--a--a-';

  expectObservable(source, sub1).toBe(expect1);
  expectObservable(source, sub2).toBe(expect2);
});

Manually unsubscribe from a source that will never complete:

ts
it('should repeat forever', () => {
  const testScheduler = createScheduler();

  testScheduler.run(({ expectObservable }) => {
    const foreverStream$ = interval(1).pipe(mapTo('a'));

    // Omitting this arg may crash the test suite.
    const unsub = '------!';

    expectObservable(foreverStream$, unsub).toBe('-aaaaa');
  });
});

Synchronous Assertion

Sometimes, we need to assert changes in state after an observable stream has completed - such as when a side effect like tap updates a variable. Outside of Marbles testing with TestScheduler, we might think of this as creating a delay or waiting before making our assertion.

For example:

ts
let eventCount = 0;

const s1 = cold('--a--b|', { a: 'x', b: 'y' });

// side effect using 'tap' updates a variable
const result = s1.pipe(tap(() => eventCount++));

expectObservable(result).toBe('--a--b|', { a: 'x', b: 'y' });

// flush - run 'virtual time' to complete all outstanding hot or cold observables
flush();

expect(eventCount).toBe(2);

In the above situation we need the observable stream to complete so that we can test the variable was set to the correct value. The TestScheduler runs in 'virtual time' (synchronously), but doesn't normally run (and complete) until the testScheduler callback returns. The flush() method manually triggers the virtual time so that we can test the local variable after the observable completes.


Known issues

RxJS code that consumes Promises cannot be directly tested

If you have RxJS code that uses asynchronous scheduling - e.g. Promises, etc. - you can't reliably use marble diagrams for that particular code. This is because those other scheduling methods won't be virtualized or known to TestScheduler.

The solution is to test that code in isolation, with the traditional asynchronous testing methods of your testing framework. The specifics depend on your testing framework of choice, but here's a pseudo-code example:

ts
// Some RxJS code that also consumes a Promise, so TestScheduler won't be able
// to correctly virtualize and the test will always be really asynchronous.
const myAsyncCode = () => from(Promise.resolve('something'));

it('has async code', (done) => {
  myAsyncCode().subscribe((d) => {
    assertEqual(d, 'something');
    done();
  });
});

On a related note, you also can't currently assert delays of zero, even with AsyncScheduler, e.g. delay(0) is like saying setTimeout(work, 0). This schedules a new "task" aka "macrotask", so it's asynchronous, but without an explicit passage of time.

Behavior is different outside of `testScheduler.run(callback)`

The TestScheduler has been around since v5, but was actually intended for testing RxJS itself by the maintainers, rather than for use in regular user apps. Because of this, some of the default behaviors and features of the TestScheduler did not work well (or at all) for users. In v6 we introduced the testScheduler.run(callback) method which allowed us to provide new defaults and features in a non-breaking way, but it's still possible to use the TestScheduler outside of testScheduler.run(callback). It's important to note that if you do so, there are some major differences in how it will behave.

  • TestScheduler helper methods have more verbose names, like testScheduler.createColdObservable() instead of cold().
  • The testScheduler instance is not automatically used by operators that use AsyncScheduler, e.g. delay, debounceTime, etc., so you have to explicitly pass it to them.
  • There is NO support for time progression syntax e.g. -a 100ms b-|.
  • 1 frame is 10 virtual milliseconds by default. i.e. TestScheduler.frameTimeFactor = 10.
  • Each whitespace ' ' equals 1 frame, same as a hyphen '-'.
  • There is a hard maximum number of frames set at 750 i.e. maxFrames = 750. After 750 they are silently ignored.
  • You must explicitly flush the scheduler.

While at this time usage of the TestScheduler outside of testScheduler.run(callback) has not been officially deprecated, it is discouraged because it is likely to cause confusion.

2. Official Technical Reference & Guides (ReactiveX/rxjs-docs)

THIS REPOSITORY IS DEPRECATED. THE RXJS DOCS ARE DEVELOPED AT THE [RXJS REPO](https://github.com/ReactiveX/rxjs)

[rxjs-docs](http://rxjsdocs.com/)

The home for new work on the new RxJS docs (RxJS v5 and up).
Website (currently beta) can be found here.
We highly welcome any kind of feedback or contribution.

License is the same as the RxJS project: https://github.com/reactivex/rxjs

Apache 2.0 License

Important

By contributing or commenting on issues in this repository, whether you've read them or not, you're agreeing to the Contributor Code of Conduct. Much like traffic laws, ignorance doesn't grant you immunity.

Goals

  • Serve updated docs for RxJs
  • Serve multiple translations for the docs
  • Provide working examples

Contributing

More detailed information can be found in the Contribution Guidelines

Building/Testing

The build and test structure is fairly primitive at the moment. There are various npm scripts that can be run:

  • start: runs the dev server. Navigate to http://localhost:4200/. The app will automatically reload if you change any of the source files.
  • test: runs tests with jasmine, must have built prior to running.
  • build: build artifacts will be stored in the dist/ directory
  • build-prod: builds for production using the --prod flag
  • commit: runs git commit wizard for passing rxjs-github-bot message validator

Committing

It is strongly recommended that when creating a commit, you follow this procedure to start the commit wizard. It will aid you on creating valid commit messages.

shell
git add .
npm run commit