skip to content

In Angular, what does EventEmitter inherit from RxJS Subject, and what goes wrong when a service exposes one as a shared event bus?

level: seniorimportance: should knowfreq 30%

answer

  1. Subject plus emit()
  2. hot, no replay, no completion
  3. anyone holding it can emit
  4. optional async delivery
  5. documented for @Output only

basics

~20 s

EventEmitter extends RxJS Subject and adds emit(): it is hot, has no current value and never completes on its own. Exposed from a service, it lets every consumer emit, error or complete it, which the Subject-based API cannot prevent.

solid answer

~40 s

`EventEmitter` in `@angular/core` is a subclass of RxJS `Subject` that adds `emit()` (which calls `next()` with no active signal consumer) and an optional async mode: `new EventEmitter(true)` delivers each value to subscribers in a `setTimeout`. Being a `Subject`, it is **hot** and multicast, keeps no current value, so late subscribers miss earlier events, and completes only if someone calls `complete()`. Its documentation says to use it in components with `@Output`. As a public field of a service it becomes a shared event bus with no encapsulation: any consumer can call `emit()`, `error()` or `complete()`, and one careless `complete()` silently ends the channel for everyone. A service should keep its `Subject` private and expose a read-only Observable. For component outputs, the `output()` function is recommended for new code.

code

ts · 25 lines
ts
import { EventEmitter, Injectable } from '@angular/core';
import { Subject } from 'rxjs';

interface Product {
  id: string;
  name: string;
}

// Fragile: every consumer can emit, error or complete the channel.
@Injectable({ providedIn: 'root' })
export class LeakyCartService {
  readonly itemAdded = new EventEmitter<Product>();
}

// Encapsulated: only the service decides what is emitted.
@Injectable({ providedIn: 'root' })
export class CartService {
  private readonly itemAddedSubject = new Subject<Product>();
  readonly itemAdded$ = this.itemAddedSubject.asObservable();

  addItem(product: Product): void {
    // ...update cart state, then notify listeners
    this.itemAddedSubject.next(product);
  }
}

go deeper

for a junior

Recall that EventEmitter is what @Output properties use and that it is a kind of RxJS Subject with an emit() method.

for a middle

Explain what EventEmitter inherits from Subject: hot, no current value, completion only on complete(), and the async option.

for a senior

Explain why a public EventEmitter in a service breaks encapsulation and how a private Subject with a read-only Observable fixes it.

for a principal

Set the codebase rule for event channels: outputs for component events, encapsulated services for shared ones, and output() for new code.

## What EventEmitter is `EventEmitter<T>` is exported from `@angular/core` and declared as extending RxJS `Subject<T>`, adding: - **`emit(value)`** — calls `Subject.next()` with Angular's active signal consumer cleared, so a handler's signal reads are never tracked by whatever reactive context happened to emit; - **an async mode** — `new EventEmitter(true)` wraps each subscriber's callbacks in `setTimeout`, delivering values after the current task and registering a pending task meanwhile; - a **subscribe signature** that accepts separate `next`, `error` and `complete` functions. When subscribed with only a `next` function, an error notification is ignored rather than reported. Its API documentation begins: "Use in components with the `@Output` directive to emit custom events". Angular also uses it internally: a reactive-forms control's `valueChanges` and `statusChanges` are `EventEmitter` instances. This answer assumes Angular 22.2 and RxJS 7.8. ## What it inherits from Subject | Property | Consequence | | --- | --- | | Hot and multicast | Every subscriber receives the same emissions; subscribing does no work | | No current value | A subscriber added after an `emit()` never sees it | | Completes only on `complete()` | It stays open for the life of whoever holds it | | Public `next()`, `error()`, `complete()` | Anyone with a reference can push into it or end it | ## The service event bus problem Picture a product-detail page where a `CartService` exposes `readonly itemAdded = new EventEmitter<Product>()`, the page's button emits into it, and a header badge subscribes. It works in a demo. The failures come later: 1. **No encapsulation.** Any component can call `cart.itemAdded.emit(fakeProduct)`. The service can no longer guarantee that emissions mean what it says. 2. **Anyone can end the channel.** A component that calls `complete()` — perhaps misreading it as "I'm done listening" — completes the emitter for every subscriber, and later emissions are silently dropped. 3. **Late subscribers see nothing.** A badge created after the first add shows zero, because the emitter has no current value. 4. **Swallowed errors.** A consumer that calls `error()` terminates the stream; subscribers that passed only a `next` function do not even see it reported. 5. **Unclear intent.** `EventEmitter` signals "component output" to every reader, so the design confuses reviewers. The usual fix keeps a private `Subject` (or `BehaviorSubject`, when late subscribers need the latest count) inside the service and exposes `asObservable()` publicly, with a method such as `addItem(product)` as the only way to emit. That pattern is covered in the topic on observable data services. ## Where output() fits For component outputs, Angular recommends the `output()` function for new projects, while `@Output()` with `EventEmitter` remains supported. `output()` returns an `OutputEmitterRef`, which has `emit()` and a `subscribe()` of its own but is **not** an RxJS Observable, so you cannot `pipe()` it. When you need an Observable from an output, `outputToObservable()` from `@angular/core/rxjs-interop` converts it, and completes the result when the owning directive is destroyed. ## Subscribing to an EventEmitter output in code Because a decorator-based output is a `Subject`, a parent that holds a child reference can write `child.panelClosed.subscribe(...)`. That subscription follows the Subject rules above: it sees only future emissions and stays open until unsubscribed or until someone completes the emitter. ## Summary - `EventEmitter` is a `Subject` with `emit()` and an optional async mode. - It is hot, has no current value and does not complete by itself. - It is documented for `@Output`; in services, keep the `Subject` private and expose an Observable.

  • What does new EventEmitter(true) change?
    It makes delivery asynchronous: each subscriber callback is wrapped in `setTimeout`, so handlers run after the current task instead of inside `emit()`. Angular also registers a pending task until the callback runs. It is rarely needed and makes ordering harder to reason about.
  • Can you pipe() an output declared with output()?
    No. `output()` returns an `OutputEmitterRef`, which has `emit()` and `subscribe()` but is not an RxJS Observable. Convert it with `outputToObservable()` from `@angular/core/rxjs-interop` when you need operators.

An EventEmitter shared from a service is like a building's public-address microphone left on the reception desk: everyone hears announcements, but anyone walking past can make one, and anyone can switch it off for the whole building.

saying these in an interview costs you the question

  • EventEmitter is a BehaviorSubject, so new subscribers get the last value.
  • EventEmitter completes automatically when its service is destroyed.
  • Only the class that created an EventEmitter can call emit() on it.
  • output() returns an EventEmitter, so it can be piped directly.
  • EventEmitter is the recommended event bus for services.