Key Takeaways
- A Promise represents one eventual outcome: fulfillment with a value or rejection with a reason. It can also remain pending.
- A Promise executor runs synchronously; its
.then()handlers run asynchronously as microtasks. - RxJS Observables can deliver zero, one, or many values, synchronously or asynchronously. Cold sources start fresh work per subscription; hot or shared sources can behave differently.
- Unsubscription stops delivery to that subscriber and runs registered teardown. Stopping underlying work requires the source to support cleanup.
- Use Promises for straightforward single-result workflows and RxJS operator pipelines for composable streams.
These distinctions follow the Promise timing rules and RxJS's subscription model.
The main difference in Observable vs Promise is the result model: a Promise represents one eventual outcome, while an RxJS Observable represents a sequence of notifications that can contain multiple values. Promises are usually simpler for one request; Observables earn their place when you need to compose repeated events and manage their lifetime.
Here, Observable means RxJS's library implementation. RxJS is one implementation of the Observable model, not a synonym for every API named Observable.
Observable vs Promise at a Glance
| Concern | Promise | RxJS Observable |
|---|---|---|
| Results | One fulfillment value or rejection reason | Zero, one, or many values per subscription |
| Starting work | The constructor calls its executor immediately | Depends on the source; cold sources create work on subscription |
| Delivery timing | Settlement handlers run as microtasks | Notifications can be synchronous or asynchronous |
| Stopping work | No built-in cancellation method; the operation may accept an abort signal | Unsubscribe; underlying cancellation depends on teardown and sharing |
| Composition | Promise chains and async/await | RxJS operator pipelines |
The important caveat is that execution timing, result count, and cancellation are separate concerns—not interchangeable definitions of asynchronous programming.
Running the Examples
The examples target RxJS 7.8.2 and its pipeable-operator API. Operators are imported from the rxjs package root, following the RxJS importing guide. Each JavaScript example is independent; don't paste all of them into the same scope.
With Node.js and npm installed, save one example as example.mjs, then run the following commands. Node treats .mjs files as ES modules; the Fetch example is separately labeled for a browser.
npm install rxjs@7.8.2
node example.mjsEager vs Lazy
A Promise Executor Runs During Construction
The function passed to new Promise() is its executor. The constructor calls it immediately, without waiting for .then(). This is the precise meaning of “eager” in this example—not a rule that every Promise controls when an external operation starts.
const myPromise = new Promise((resolve) => {
console.log('Promise firing!');
resolve('Promise resolved.');
});
console.log('Before calling then...');
myPromise.then((value) => console.log(value));Promise firing!
Before calling then...
Promise resolved.A Cold Observable Defers Its Work Until Subscription
The callback passed to this Observable constructor doesn't run until you subscribe. Calling complete() ends this finite sequence after its value is delivered.
import { Observable } from 'rxjs';
const myObservable$ = new Observable((observer) => {
console.log('Observable firing!');
observer.next('Observable called');
observer.complete();
});
console.log('Before calling subscribe on Observable');
myObservable$.subscribe({ next: (value) => console.log(value) });Before calling subscribe on Observable
Observable firing!
Observable calledCold, Hot, and Shared Sources
A cold Observable creates a new producer—the code or resource generating values—for each subscription. Subscribe twice to the example above and its callback runs twice. A hot Observable uses a producer created outside subscription, such as an existing event source.
Sharing lets multiple subscribers observe one producer. Consequently, subscribing doesn't universally mean “start independent work,” and removing one subscriber doesn't necessarily stop work needed by others. The RxJS glossary makes these distinctions explicit.
You can also defer Promise creation by putting it inside a function. The difference is between an already-created Promise and a recipe for creating work, not an inability to control your own function calls.
Sync vs Async
Promise Handlers Are Asynchronous, Even After Immediate Fulfillment
Promise executor work can be synchronous, but handlers registered with .then() run as microtasks after the current synchronous execution finishes. An already-fulfilled Promise doesn't change that rule.
const myPromise = new Promise((resolve) => {
resolve('Promise resolved.');
});
console.log('Before calling then...');
myPromise.then((value) => console.log(value));
console.log('After calling then...');Before calling then...
After calling then...
Promise resolved.Observable Notifications Can Be Synchronous
This source delivers its value before subscribe() returns. Observable subscription callbacks aren't automatically scheduled as microtasks.
import { Observable } from 'rxjs';
const myObservable$ = new Observable((observer) => {
observer.next('Observable called');
observer.complete();
});
console.log('Before calling subscribe');
myObservable$.subscribe({ next: (value) => console.log(value) });
console.log('After calling subscribe');Before calling subscribe
Observable called
After calling subscribeA Timer-Backed Observable Delivers Later
Here, subscription schedules a timer synchronously, but the timer delivers the value later. The returned function is teardown: cleanup that clears the timer when the subscription is disposed.
import { Observable } from 'rxjs';
const delayed$ = new Observable((observer) => {
const timer = setTimeout(() => {
observer.next('Delayed value');
observer.complete();
}, 1000);
return () => clearTimeout(timer);
});
console.log('Before subscribing');
delayed$.subscribe({
next: (value) => console.log(value),
complete: () => console.log('Complete'),
});
console.log('After subscribing');Before subscribing
After subscribing
Delayed value
CompleteThe timing distinction looks like this; each row describes its corresponding example, not one combined program.
Current synchronous execution
Promise: executor runs; then() registers a handler
Sync Observable: subscribe() delivers next and complete
Timer Observable: subscribe() schedules a timer
Microtask checkpoint
Promise: then() handler runs
Later timer task
Timer Observable: next and complete are deliveredRxJS Operators: Compose Values, Timing, and Lifetime
RxJS supplies operators for composing Observables. A pipeable operator takes a source Observable and returns another Observable; pipe() connects those transformations without modifying the original source.
Start with the original arithmetic example: map() transforms the emitted 2 into 4.
import { Observable, map } from 'rxjs';
const values$ = new Observable((observer) => {
observer.next(2);
observer.complete();
});
values$.pipe(map((value) => value * 2)).subscribe({
next: (value) => console.log(value),
});4The benefit becomes clearer with an ongoing source. This periodic refresh counter keeps every second tick, then stops after three retained values:
import { filter, interval, map, take } from 'rxjs';
interval(500).pipe(
map((tick) => tick + 1),
filter((count) => count % 2 === 0),
take(3),
).subscribe({
next: (count) => console.log(count),
error: (error) => console.error(error),
complete: () => console.log('Complete'),
});2
4
6
Completeinterval(500) produces ascending numbers starting at zero, with its first emission after the first interval. filter() keeps matching values, and take(3) completes after three values reach it. This combines timing, transformation, filtering, and lifetime management in one pipeline.
Cancellability: Unsubscription vs Aborting an Operation
Observable Unsubscription Needs Working Teardown
subscribe() returns a Subscription. Calling unsubscribe() stops notifications to that subscriber and invokes registered teardown; the teardown is what releases resources such as timers.
import { Observable } from 'rxjs';
const ticks$ = new Observable((observer) => {
let count = 0;
const timer = setInterval(() => observer.next(++count), 1000);
return () => {
clearInterval(timer);
console.log('Interval cleared');
};
});
const subscription = ticks$.subscribe({
next: (value) => console.log('Tick:', value),
complete: () => console.log('Complete'),
});
setTimeout(() => subscription.unsubscribe(), 2500);You'll see ticks followed by Interval cleared. Manual unsubscription does not invoke the observer's complete callback. Unsubscribing after a synchronous source has already delivered its values cannot undo those deliveries.
A Promise-Based API Can Support Abort Signals
A Promise has no built-in cancellation method. However, fetch() accepts an AbortSignal, so you can abort the operation separately from the Promise representing its result. The Fetch guide documents both cancellation and response-status handling.
Run this independent example in a browser console on an HTTP(S) page. It requests the current page, then immediately aborts:
async function readPage(signal) {
const response = await fetch(window.location.href, { signal });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.text();
}
const controller = new AbortController();
const task = readPage(controller.signal);
task.then((text) => console.log(text.length)).catch((error) => {
if (error.name === 'AbortError') {
console.log('Request aborted');
} else {
console.error('Request failed', error);
}
});
controller.abort();HTTP error responses don't automatically reject the Fetch Promise, which is why the example checks response.ok. Aborting the request produces an AbortError; it doesn't add a cancellation method to task.
Emitting Multiple Values
An Observable can deliver zero, one, or many next notifications, then complete, error, or remain open. Completion and error are terminal: neither permits further notifications to that subscription.
import { Observable } from 'rxjs';
const numbers$ = new Observable((observer) => {
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
});
numbers$.subscribe({ next: (value) => console.log(value) });1
2
3A Promise settles at most once. In this example, only the first resolve() call affects the outcome; subsequent calls are ignored.
const myPromise = new Promise((resolve) => {
resolve(1);
resolve(2);
resolve(3);
});
myPromise.then((value) => console.log(value));1A Promise can fulfill with an array containing many items, but that array is still one fulfillment value—not a sequence of separate deliveries.
Error Handling and Cleanup
Handle Promise rejection with .catch() or try/catch around await. For an Observable, provide an error handler. An error terminates that subscription without a completion notification, and registered teardown still runs.
import { Observable } from 'rxjs';
Promise.reject(new Error('Promise failed')).catch((error) => {
console.error(error.message);
});
const failing$ = new Observable((observer) => {
observer.next('Before failure');
observer.error(new Error('Stream failed'));
return () => console.log('Observable cleanup');
});
failing$.subscribe({
next: (value) => console.log(value),
error: (error) => console.error(error.message),
complete: () => console.log('Not called after an error'),
});For application code, put cleanup in teardown rather than only in a complete handler: completion, failure, and manual unsubscription are different ways a subscription can end.
Converting a Promise to an Observable
RxJS from(promise) adapts fulfillment into a value followed by completion, or rejection into an error notification. It doesn't restart the operation or make the underlying Promise cancellable. Unsubscription can suppress delivery to that subscriber while the original work continues.
Use defer() when you want the Promise-producing function called at subscription time:
import { defer, from } from 'rxjs';
function makePromise() {
return new Promise((resolve) => {
console.log('Work started');
resolve(42);
});
}
const eager$ = from(makePromise()); // Work starts here.
const lazy$ = defer(makePromise); // The factory hasn't run yet.
const observer = {
next: (value) => console.log(value),
error: (error) => console.error(error),
};
eager$.subscribe(observer);
lazy$.subscribe(observer); // Creates another Promise here.Each subscription to lazy$ calls makePromise() again. By contrast, subscribers to eager$ observe the same already-created Promise. Deferral changes when work starts, not whether that work supports cancellation.
Which Should You Use?
- One request or one eventual result: start with a Promise and
async/await. See our JavaScript async/await example for that workflow. - Ongoing events or repeated values: consider an Observable when filtering, transforming, and controlling a stream's lifetime belong in one pipeline. Our guide to when to use an Observable explores that choice.
- An existing RxJS application: keeping a single-result operation in an Observable pipeline can still be sensible. Result count is a useful starting point, not the only design constraint.
For WebSocket messages and repeated UI events, Observables fit the repeated-notification model. For a straightforward Fetch result, a Promise is usually enough. Neither abstraction replaces the other; choose the interface that makes your data flow and cleanup easiest to understand.
Common Mistakes
- Calling all Promise code asynchronous. Separate executor timing from handler timing.
- Assuming subscriptions share work. Two subscriptions to a cold source create two producers.
- Unsubscribing without implementing teardown. Closing delivery doesn't automatically clear your timer or abort your request.
- Using completion as a cancellation callback. Manual unsubscription doesn't send a completion notification.
These mistakes all come from overlooking the boundary between a subscription, its producer, and the underlying operation.
FAQ
When should I use Promises over Observables?
Use a Promise when you need one eventual result, particularly with Fetch and async/await. Observables are useful for ongoing event streams and workflows that benefit from RxJS operator composition.
Are Promise executors synchronous and .then() callbacks asynchronous?
Yes. The executor passed to new Promise() runs synchronously during construction, while .then() callbacks run asynchronously as microtasks, even when the Promise is already fulfilled.
Can you cancel a Promise or an RxJS Observable?
A Promise has no built-in cancellation method, but its underlying operation may support AbortController. An Observable subscription can be unsubscribed; whether that stops underlying work depends on the source's teardown and sharing behavior.
Can I convert a Promise to an Observable?
Yes: use RxJS from(promise) to adapt its result into Observable notifications. Unsubscribing does not cancel the original Promise's work; use defer() if you also need to delay Promise creation until subscription.
Are Observables replacing Promises in modern web development?
No. Promises and Observables solve different interface problems: one eventual outcome versus a potentially ongoing sequence of notifications. Use whichever makes the workflow simpler rather than converting everything to one abstraction.
Further Reading
- MDN: Promise constructor — executor timing, resolution, and settlement.
- MDN: Using promises — chaining, error handling, and microtask timing.
- RxJS: Observable guide — synchronous and asynchronous notifications and the Observable contract.
- RxJS: Glossary and semantics — producers, cold sources, hot sources, and multicasting.
- RxJS: Subscription reference — disposal and teardown behavior.
- RxJS: Importing instructions — package-root imports used in these examples.
- MDN: Using Fetch — response checks and cancellation with abort signals.
