An Observable is the core building block of RxJS. It represents a lazy Push collection of multiple values that can be delivered synchronously or asynchronously over time.
What is an Observable?
Observables fill a unique role in JavaScript’s data production paradigm:
| Single | Multiple |
|---|
| Pull | Function | Iterator |
| Push | Promise | Observable |
Key Insight: An Observable is like a function that can return multiple values over time, both synchronously and asynchronously.
Pull vs Push Systems
Pull Systems: The consumer decides when to receive data from the producer.
- Functions: You call them to get a single value
- Iterators: You call
next() to get multiple values
Push Systems: The producer decides when to send data to the consumer.
- Promises: Deliver one value when ready
- Observables: Deliver zero to infinite values when ready
Type Signature
class Observable<T> {
constructor(subscribe?: (subscriber: Subscriber<T>) => TeardownLogic)
subscribe(observer: Partial<Observer<T>>): Subscription
subscribe(next: (value: T) => void): Subscription
pipe<A>(op1: OperatorFunction<T, A>): Observable<A>
pipe<A, B>(op1: OperatorFunction<T, A>, op2: OperatorFunction<A, B>): Observable<B>
// ... additional pipe overloads
}
type TeardownLogic = Subscription | Unsubscribable | (() => void) | void
Creating Observables
Using the Constructor
The Observable constructor takes a subscribe function that defines how values are produced:
import { Observable } from 'rxjs';
const observable = new Observable((subscriber) => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
setTimeout(() => {
subscriber.next(4);
subscriber.complete();
}, 1000);
});
Using Creation Operators
Most commonly, you’ll create Observables using creation operators like of, from, interval, etc.
import { of } from 'rxjs';
const numbers$ = of(1, 2, 3, 4, 5);
Subscribing to Observables
With Observer Object
With Next Function
Partial Observer
observable.subscribe({
next(x) {
console.log('got value ' + x);
},
error(err) {
console.error('something wrong occurred: ' + err);
},
complete() {
console.log('done');
},
});
observable.subscribe(x => console.log('got value ' + x));
// Only handle next and error
observable.subscribe({
next: x => console.log(x),
error: err => console.error(err)
});
Subscribing to an Observable starts its execution. Each subscription creates an independent execution.
Observable Execution
Notification Types
An Observable execution can deliver three types of notifications:
- Next: Sends a value (Number, String, Object, etc.)
- Error: Sends a JavaScript Error (terminates the execution)
- Complete: Sends no value (terminates the execution)
Observable Contract
All Observable executions follow this pattern (expressed as a regular expression):
This means:
- Zero to infinite Next notifications
- Optionally followed by either one Error OR one Complete
- Nothing can be delivered after Error or Complete
import { Observable } from 'rxjs';
const observable = new Observable((subscriber) => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
subscriber.complete();
subscriber.next(4); // Never delivered - violates the contract
});
Synchronous vs Asynchronous
Observables can deliver values either synchronously or asynchronously - they are not inherently async.
import { Observable } from 'rxjs';
const sync$ = new Observable((subscriber) => {
subscriber.next(42);
});
console.log('before');
sync$.subscribe(x => console.log(x));
console.log('after');
// Output:
// before
// 42
// after
Disposing Observable Executions
Subscribing returns a Subscription object that can be used to cancel the execution:
import { interval } from 'rxjs';
const subscription = interval(1000).subscribe(x => console.log(x));
// Later: stop the execution
subscription.unsubscribe();
Cleanup Function
When creating an Observable, return a cleanup function to release resources:
import { Observable } from 'rxjs';
const observable = new Observable((subscriber) => {
const intervalId = setInterval(() => {
subscriber.next('hi');
}, 1000);
// Cleanup function
return () => {
clearInterval(intervalId);
};
});
const subscription = observable.subscribe(x => console.log(x));
// Calling unsubscribe() will execute the cleanup function
subscription.unsubscribe();
Observables vs Functions
Observables are like functions with superpowers:
function foo() {
console.log('Hello');
return 42;
// Cannot return another value
}
const x = foo(); // 42
const y = foo(); // 42
import { Observable } from 'rxjs';
const foo = new Observable((subscriber) => {
console.log('Hello');
subscriber.next(42);
subscriber.next(100); // Can "return" multiple values
subscriber.next(200);
});
foo.subscribe(x => console.log(x)); // 42, 100, 200
foo.subscribe(y => console.log(y)); // 42, 100, 200
Both functions and Observables are lazy - they don’t execute until called/subscribed.
Observables vs Promises
| Feature | Promise | Observable |
|---|
| Values | Single value | Multiple values |
| Lazy | No (eager) | Yes (lazy) |
| Cancellable | No | Yes (via unsubscribe) |
| Operators | Limited (then, catch) | Extensive library |
| Multicast | Yes | No (unless using Subject) |
Common Patterns
Error Handling
import { Observable } from 'rxjs';
const observable = new Observable((subscriber) => {
try {
subscriber.next(1);
subscriber.next(2);
// Risky operation
throw new Error('Something went wrong');
} catch (err) {
subscriber.error(err);
}
});
observable.subscribe({
next: x => console.log(x),
error: err => console.error('Error:', err.message)
});
Finite Streams
import { Observable } from 'rxjs';
const finite$ = new Observable<number>((subscriber) => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
subscriber.complete(); // Signal completion
});
Infinite Streams
import { interval } from 'rxjs';
const infinite$ = interval(1000); // Never completes
// Must unsubscribe to stop
Best Practices
Naming Convention: Suffix Observable variable names with $ (e.g., data$, clicks$) to distinguish them from regular values.
- Always handle errors - Unhandled errors can crash your application
- Unsubscribe when done - Prevent memory leaks in long-lived components
- Use creation operators - Prefer
of, from, etc. over new Observable
- Keep subscribe logic minimal - Use operators for transformations
- Return cleanup functions - Always clean up resources like timers and event listeners
When to Use Observables
Use Observables when you need:
- Multiple values over time
- Cancellation support
- Lazy execution
- Powerful composition via operators
- Complex async coordination
Examples:
- User input events
- WebSocket messages
- HTTP polling
- Real-time data streams
- Animation sequences
- Observer - Consumes values from Observables
- Subscription - Manages Observable execution lifecycle
- Operators - Transform and compose Observables
- Subject - Special Observable that multicasts to multiple Observers