---
# System prepended metadata

title: AbortController as Subscription

---

AbortController as Subscription
===

Currently AbortController and AbortSignal exist in all browser platforms, and there appears to be work to get it added to Node as well. AbortController and AbortSignal are a part of the Fetch API and they allow cancellation. Together, they are *very* similar to RxJS's `Subscription` class.

The intent of this document is to explore the possiblity that Subscription could be either replaced by AbortController or created from AbortController.

## Similarities

|Subscription|AbortController
|:-----|:---
|`x.unsubscribe()`|`x.abort()`
|`x.add(fn)`[^1]|`x.signal.addEventListener('abort', fn)`
|`x.remove(fn)`[^2]|`x.signal.removeEventListener('abort', fn)`
|`x.closed`|`x.signal.aborted`

[^1]: `Subscription#add` in RxJS will accept `Subscription`, `() => void`, or `void`. It also has the difference that it will execute any of those provided teardowns immediately if the subscription happens to be closed. `AbortController`/`AbortSignal` has no such behavior. In RxJS v6 it also always returns a `Subscription`. This is so there's something to be passed to `Subscription#remove` if a function is passed to `add`.
[^2]: `Subscription#remove` in RxJS v6 only removes Subscriptions, not functions. This should probably change in the future.

## Option 1: AbortController replacing Subscription

In this scenario, we would make `Subscription` be `AbortController`-shaped, and migrate people's code over to a world where `subscribe` returned an `AbortController` instead.

### Pros

- We ship one less type to the browser/Node.
- The return value of a subscription can be used to signal things like `fetch` to abort.
- A unified, platform provided, cancellation type for all.

### Cons

- Substantially less ergonomic for the "Composite Subscription" use-case, which is very common.
- `subscribe`/`abort` makes a lot less sense than `subscribe`/`unsubscribe`.
- Adding a handler to an already `aborted` `AbortSignal` will not execute the handler. This means less safety in scenarios where subscriptions are being programmatically created, as opposed to the behavior of `Subscription#add`, which will unsubscribe the passed teardown immediately if the subscription instance is `closed`.

## Option 2: Subscription derives from AbortController

This is actually probably a transitional point for option 1 above. But in this scenario, `Subscription extends AbortController`. I'm not sure this is totally possible without "real ES6 classes" rather than compiled ES5 classes, but it seems like it should work.

### Pros

- Provides all the same safety and ergonomics of `Subscription`.
- Can leave `unsubscribe`, but deprecate it.
- Subscriptions themselves will have a usable `signal` property that can be used with other APIs expecting this type.

### Cons

- Broader, more confusing API. Docs may be more confusing.
- `add` and `signal.addEventListener('abort',...)` are not equivalent in behavior, and that could confuse people.
- Still shipping a custom class to the browser, so it's not as much of a win.


## Transitional Class Example

This is just a rough idea of what a Subscription that extended AbortController might look like:

```typescript=

class Subscription extends AbortController {
  private _lookup = new Map<any, any>();

  /** @deprecated use `signal.aborted` instead. */
  get closed() {
    return this.signal.aborted;
  }

  /** @deprecated use `abort` */
  unsubscribe() {
    this.abort();
  }

  /** @deprecated use `signal.addEventListener('abort', fn)` */
  add(child: AbortController|(() => void)|undefined): void {
    const { _lookup, signal } = this;
    if (!child || _lookup.has(child)) return;

    if (signal.aborted) {
      if (typeof child === 'function') {
        child();
      } else {
        child.abort();
      }
      return;
    }

    if (typeof child !== 'function') {
      child.signal.addEventListener('abort', () => this.remove(child));
    }

    const handler = () => {
      if (typeof child === 'function') {
        child();
      } else {
        child.abort();
      }
      this.remove(child);
    };

    _lookup.set(child, handler);

    signal.addEventListener('abort', handler);
  }

  /** @deprecated use `signal.removeEventListener('abort', fn)` */
  remove(child: AbortController|(() => void)|undefined): void {
    const { signal, _lookup } = this;
    if (child && _lookup.has(child)) {
      signal.removeEventListener('abort', _lookup.get(child));
      _lookup.delete(child);
    }
  }
}
```