Skip to main content

Breaking Changes in RxJS v7

This page documents all breaking changes introduced in RxJS v7. Understanding these changes is critical for successfully migrating from v6.

General Breaking Changes

TypeScript Version Requirement

RxJS v7 requires TypeScript 4.2 or higher. Earlier versions of TypeScript are not supported.
npm install -D typescript@^4.2.0

rxjs-compat Removed

The rxjs-compat package is not published for v7. You must migrate away from all deprecated v6 APIs before upgrading.

Observable Changes

lift No Longer Exposed

The lift operator is no longer publicly exposed. Create custom operators using the documented pattern:
import { Observable } from 'rxjs';

function customOperator() {
  return (source: Observable<any>) => {
    return (source as any).lift(/* ... */); // TypeScript error!
  };
}

Internal Methods Protected

Several internal methods are no longer public:
  • Observable._subscribe is now @internal
  • Observable._trySubscribe is now @internal
  • Observable._isScalar property removed
  • Observable.if and Observable.throw static properties removed

pipe Return Type Changes

import { of } from 'rxjs';
import { map } from 'rxjs/operators';

// 9+ arguments returned Observable<{}>
const result = of(1).pipe(
  map(x => x),
  map(x => x),
  map(x => x),
  map(x => x),
  map(x => x),
  map(x => x),
  map(x => x),
  map(x => x),
  map(x => x)
); // Observable<{}>

toPromise Return Type

Breaking: toPromise() now correctly returns Promise<T | undefined> instead of Promise<T>.
import { EMPTY } from 'rxjs';

const result: Promise<number> = EMPTY.toPromise();
// result resolves to undefined but type says number

Subscription Changes

add() Returns void

Breaking: Subscription.add() no longer returns a Subscription. It now returns void.
import { Subscription, interval } from 'rxjs';

const subscription = new Subscription();
const sub1 = subscription.add(
  interval(1000).subscribe()
);
// sub1 is a Subscription
subscription.remove(sub1);

Removed Internal Properties

  • Subscription._parentOrParents property removed
  • Subscriber._unsubscribeAndRecycle method removed
  • Subscriber.syncErrorThrowable property removed
  • Subscriber.syncErrorThrown property removed
  • Subscriber.syncErrorValue property removed

Subscriber Changes

Constructor Signature Changed

Breaking: new Subscriber() no longer accepts 0-3 function arguments. Use Subscriber.create() instead (though both are deprecated).
import { Subscriber } from 'rxjs';

// Broken in v7
const subscriber = new Subscriber(
  x => console.log(x),
  err => console.error(err),
  () => console.log('complete')
);

// Use Subscriber.create (deprecated) or Observer objects
const subscriber = Subscriber.create(
  x => console.log(x),
  err => console.error(err),  
  () => console.log('complete')
);

this Context No Longer Available

The this context in observer functions no longer provides access to unsubscribe().
import { of } from 'rxjs';

of(1, 2, 3).subscribe(function(value) {
  console.log(value);
  if (value === 2) {
    this.unsubscribe(); // Works in v6
  }
});

Subject Changes

  • _subscribe method is now protected (was public)
  • No longer has its own error method implementation
import { AsyncSubject } from 'rxjs';

const subject = new AsyncSubject();
// subject._subscribe() // TypeScript error in v7
  • _subscribe method is now protected (was public)
  • value is now a getter instead of readonly property
  • Cannot be forcibly set
import { BehaviorSubject } from 'rxjs';

const subject = new BehaviorSubject(0);
console.log(subject.value); // Still works
// (subject as any).value = 1; // Won't work like it might have in edge cases
  • _subscribe method is now protected (was public)
  • _getNow method removed
  • No longer schedules emissions when a scheduler is provided
import { ReplaySubject, asapScheduler } from 'rxjs';

const subject = new ReplaySubject(2, 3000, asapScheduler);
// Emissions are scheduled

Operator Breaking Changes

Creation Operators

defer no longer allows factories to return void or undefined. Must return a valid ObservableInput.
import { defer } from 'rxjs';

const source$ = defer(() => {
  console.log('side effect');
  // Returns undefined - breaks in v7!
});
Signatures changed - separate overloads for each target type. Passing options to incompatible targets now causes TypeScript errors.
import { fromEvent } from 'rxjs';

const clicks$ = fromEvent(document, 'click', {
  once: true // Type-checked against DOM EventTarget
});
iif no longer allows undefined as result arguments.
import { iif } from 'rxjs';

const result$ = iif(
  () => Math.random() > 0.5,
  undefined, // Breaks in v7!
  of('false')
);
No longer has a generic parameter. Returns Observable<unknown> - you must cast the result.
import { isObservable, of } from 'rxjs';

const value = of(1);
if (isObservable<number>(value)) {
  // value is Observable<number>
}
pairs requires Object.entries polyfill for IE. Consider using from(Object.entries(obj)) instead.
import { from } from 'rxjs';

const obj = { a: 1, b: 2, c: 3 };
from(Object.entries(obj)).subscribe(console.log);
race no longer subscribes to subsequent observables if a source synchronously errors or completes.
import { race, of, throwError } from 'rxjs';
import { tap } from 'rxjs/operators';

race(
  throwError(() => new Error('immediate')),
  of(1).pipe(tap(() => console.log('This side effect won\'t run')))
).subscribe();

Pipeable Operators

The following operators now require their duration selectors to emit a next notification, not just complete:
  • audit - duration must emit next
  • debounce - duration must emit next
  • throttle - duration must emit next
  • bufferToggle - closing selector must emit next
  • bufferWhen - closing selector must emit next
  • windowToggle - closing selector must emit next
  • delayWhen - duration must emit value, not just complete
  • sample - notifier must emit next, not just complete
import { interval, audit, EMPTY } from 'rxjs';

interval(100).pipe(
  audit(() => EMPTY) // EMPTY just completes - won't trigger audit!
).subscribe();
Multiple behavioral changes:
  • Now subscribes to source before closing notifier (order reversed)
  • Final buffered values always emitted
  • Closing notifier completion no longer completes result
import { interval, buffer } from 'rxjs';
import { take } from 'rxjs/operators';

const notifier$ = interval(1000).pipe(take(3));
interval(100).pipe(buffer(notifier$)).subscribe();
// Completes when notifier completes
Predicate no longer receives source observable as third argument.
import { of } from 'rxjs';
import { count } from 'rxjs/operators';

of(1, 2, 3).pipe(
  count((value, index, source) => {
    console.log(source); // Had access to source
    return value > 1;
  })
);
defaultIfEmpty requires a value argument. No longer converts undefined to null.
import { EMPTY } from 'rxjs';
import { defaultIfEmpty } from 'rxjs/operators';

EMPTY.pipe(
  defaultIfEmpty() // Error in v7!
);
Finalize callbacks now run in pipeline order (not reversed).
import { of } from 'rxjs';
import { finalize } from 'rxjs/operators';

of(1).pipe(
  finalize(() => console.log('1')),
  finalize(() => console.log('2'))
).subscribe();
// Logs: "2" then "1" (reversed)
thisArg now defaults to undefined instead of MapSubscriber.
import { of } from 'rxjs';
import { map } from 'rxjs/operators';

// Only affects function (not arrow function) with 'this' reference
of(1, 2, 3).pipe(
  map(function(value) {
    console.log(this); // undefined in v7, was MapSubscriber in v6
    return value * 2;
  })
);
mergeScan no longer emits inner state again upon completion.
import { of } from 'rxjs';
import { mergeScan } from 'rxjs/operators';

of(1, 2, 3).pipe(
  mergeScan((acc, value) => of(acc + value), 0)
).subscribe(console.log);
// v6: 1, 3, 6, 6 (duplicated final value)
// v7: 1, 3, 6 (no duplicate)
single now throws more specific errors. Check error handling logic.
import { of, empty } from 'rxjs';
import { single } from 'rxjs/operators';

// Throws EmptyError if no values
empty().pipe(single()).subscribe();

// Throws SequenceError if multiple values
of(1, 2, 3).pipe(single()).subscribe();

// Throws NotFoundError if predicate never matches
of(1, 2, 3).pipe(
  single(x => x > 10)
).subscribe();
skipLast no longer errors with negative numbers - returns source instead.
import { of } from 'rxjs';
import { skipLast } from 'rxjs/operators';

// v6: throws error
// v7: returns source unchanged
of(1, 2, 3).pipe(
  skipLast(-1)
).subscribe(console.log); // 1, 2, 3
take now throws runtime errors for negative or NaN arguments.
import { of } from 'rxjs';
import { take } from 'rxjs/operators';

of(1, 2, 3).pipe(
  take(-1) // Throws TypeError in v7
).subscribe();

of(1, 2, 3).pipe(
  take(NaN) // Throws TypeError in v7  
).subscribe();
takeLast throws TypeError for invalid arguments (no args or NaN).
import { of } from 'rxjs';
import { takeLast } from 'rxjs/operators';

of(1, 2, 3).pipe(
  takeLast() // Throws TypeError
).subscribe();
windowBoundaries no longer completes the result.
import { interval } from 'rxjs';
import { window, endWith, skipLast, take } from 'rxjs/operators';

const notifier$ = interval(1000).pipe(take(3));

interval(100).pipe(
  window(notifier$.pipe(endWith(true))),
  skipLast(1)
).subscribe();
Multiple changes to zip behavior:
  • Zipping single array has different result
  • Iterables no longer consumed “as needed” (can lock up with infinite iterables)
import { zip, of } from 'rxjs';

function* infiniteGenerator() {
  let i = 0;
  while (true) yield i++;
}

// v7: Locks up trying to read entire iterable!
zip(of(1, 2, 3), infiniteGenerator()).subscribe();

Ajax Module Changes

Breaking: Ajax body serialization now uses default XHR behavior. Custom Content-Type headers no longer affect serialization.

Body Serialization

import { ajax } from 'rxjs/ajax';

// Automatically JSON serialized (objects)
ajax({
  url: '/api/data',
  method: 'POST',
  body: { name: 'John' } // Auto-serialized, Content-Type set
}).subscribe();

// Default handling (FormData, Blob, ArrayBuffer, etc.)
ajax({
  url: '/api/upload',
  method: 'POST',
  body: new FormData() // Uses XHR default handling
}).subscribe();

Type Changes

For TypeScript users: Use AjaxConfig instead of AjaxRequest for ajax configuration.
import { ajax, AjaxConfig } from 'rxjs/ajax';

const config: AjaxConfig = {
  url: '/api/data',
  method: 'GET',
  // AjaxConfig includes progressSubscriber and createXHR
};

ajax(config).subscribe();

IE10 Support Dropped

Ajax module no longer supports IE10 and lower.

Testing Module Changes

New TestScheduler Feature

import { TestScheduler } from 'rxjs/testing';
import { of } from 'rxjs';
import { map } from 'rxjs/operators';

const scheduler = new TestScheduler((actual, expected) => {
  expect(actual).toEqual(expected);
});

scheduler.run(({ expectObservable }) => {
  const source$ = of(1, 2, 3);
  const result$ = source$.pipe(map(x => x * 2));
  
  // New toEqual() method for refactoring verification
  expectObservable(result$).toEqual(
    expectObservable(source$.pipe(map(x => x * 2)))
  );
});

Error Handling Changes

Unhandled Errors

Errors during subscription setup (after error/complete) now throw in their own call stack instead of using console.warn.
import { config } from 'rxjs';

// Restore v6 behavior if needed (not recommended)
config.onUnhandledError = (err) => console.warn(err);

Error Stack Properties

RxJS errors now have proper stack properties. May affect test assertions using deep equality.
import { throwError } from 'rxjs';

throwError(() => new Error('test')).subscribe({
  error(err) {
    console.log(err.stack); // Proper stack trace in v7
  }
});

Removed Features

The following features have been completely removed:
  • Experimental for await support (use https://github.com/benlesh/rxjs-for-await)
  • rxjs/Rx import site (from rxjs-compat)
  • Notification.createNext(undefined) singleton behavior
  • VirtualTimeScheduler.sortActions static method (no longer public)

Migration Resources