Key Takeaways
- A Promise settles once: it fulfills with one value or rejects with one reason. An RxJS Observable can deliver zero or more values.
- A Promise executor runs immediately, but its handlers run asynchronously. Observable notifications can be synchronous or asynchronous, depending on the source and scheduling.
- A cold Observable creates independent work for each subscription. Multicasting means sharing one producer, not running separate producers.
- Unsubscribing stops notifications and runs registered teardown. Actually canceling a timer or request requires cleanup that supports cancellation.
The difference between an Observable and a Promise is the result model: a Promise represents one eventual result, while an RxJS Observable models a stream of values and its subscription lifecycle. Start with a Promise for a single asynchronous operation; choose an Observable when repeated values, stream composition, or explicit subscription management earn the extra abstraction.
Observable vs Promise at a Glance
| Concern | Native Promise | RxJS Observable |
|---|---|---|
| Results | One fulfillment value or rejection reason | Zero or more next notifications; optional completion or error |
| Starting work | The constructor calls its executor immediately | Cold sources create work when subscribed |
| Notification timing | then/catch handlers run asynchronously | Depends on the source and scheduling |
| Cleanup | No built-in cancellation method; underlying APIs may support aborting | Subscription teardown can release resources or cancel supported work |
| Multiple consumers | Handlers on the same Promise observe the same settlement | Independent or shared execution, depending on the source |
| Composition | Promise chains for eventual results | Operators for transforming and coordinating streams |
These are separate concerns: lazy execution doesn't imply asynchronous delivery, and multiple subscribers don't automatically imply shared execution.
What Is a Promise?
A Promise is built into JavaScript. It starts pending and can settle as either fulfilled with a value or rejected with a reason; once settled, its outcome doesn't change.
Promise Example: One Delayed Result
const promise = new Promise((resolve) => {
setTimeout(() => {
resolve('some value');
}, 1000);
});
promise
.then((value) => console.log(value))
.catch((error) => console.error('Promise failed:', error));Output when the timer fires:
some valueThe executor—the function passed to new Promise()—runs immediately and schedules the timer. The result arrives later. Use resolve(value) to resolve the Promise, reject(reason) to reject it, and .catch() to handle rejection.
What Is an Observable?
An Observable connects a consumer to a producer: the code or resource supplying values. This article uses RxJS, the Reactive Extensions Library for JavaScript, which adds an Observable implementation and stream operators as a dependency; the broader Observable pattern isn't exclusive to RxJS. For another introduction, see JavaScript Observables in 5 Minutes.
Run the RxJS Examples
Install the package, save one example at a time as example.mjs, and run it with Node.js. The .mjs extension explicitly enables ECMAScript modules, including the imports shown below.
npm install rxjs
# Save an example below as example.mjs, then run:
node example.mjsObservable Example: A Delayed Value and Completion
import { Observable } from 'rxjs';
const observable = new Observable((observer) => {
const timer = setTimeout(() => {
observer.next('some value');
observer.complete();
}, 1000);
return () => clearTimeout(timer);
});
observable.subscribe({
next: (value) => console.log(value),
error: (error) => console.error('Observable failed:', error),
complete: () => console.log('done'),
});Output when the timer fires:
some value
doneIn this cold Observable, subscribing creates the timer. The returned function is teardown: cleanup registered for the subscription. subscribe() returns a Subscription that lets the consumer unsubscribe.
Completion, Errors, and Subscription Lifetimes
next(value) delivers a value, complete() signals successful termination without another value, and error(reason) signals failure. Completion and error are mutually exclusive terminal notifications; neither allows subsequent values. A source may also remain open indefinitely.
Promise:
construction -> executor runs -> pending -> fulfilled or rejected
|
v
then/catch runs asynchronously
Cold RxJS Observable:
subscribe -> producer -> next ... next -> complete or error
| |
+------ unsubscribe ---------+--> teardownTeardown runs when a subscription completes, errors, or is unsubscribed. Unsubscribing itself doesn't call the observer's complete handler.
Difference Between Observables and Promises: Examples
Single Settlement vs Multiple Values
Calling a Promise's resolving function again doesn't provide another result. This example prints only a.
const promise = new Promise((resolve) => {
resolve('a');
resolve('b'); // Ignored.
});
promise.then((value) => console.log(value));
// Output: aAn Observable can deliver both values before completing.
import { Observable } from 'rxjs';
const observable = new Observable((observer) => {
observer.next('a');
observer.next('b');
observer.complete();
});
observable.subscribe({
next: (value) => console.log(value),
complete: () => console.log('done'),
});
// Output:
// a
// b
// doneA Promise can fulfill with an array or another object containing many items. That's still one fulfillment value, not a stream of separate notifications.
Eager vs Lazy Execution
A Promise constructor invokes its executor synchronously; it doesn't wait for a handler to be attached. Its then callback runs later, even when the Promise fulfills immediately.
const promise = new Promise((resolve) => {
console.log('promise is running');
resolve('a');
});
console.log('start');
promise.then((value) => console.log(value));
console.log('end');promise is running
start
end
aThe subscription function in this cold Observable runs only when you subscribe. Because it emits synchronously, the value appears before end.
import { Observable } from 'rxjs';
const observable = new Observable((observer) => {
console.log('observable is running');
observer.next('a');
observer.complete();
});
console.log('start');
observable.subscribe((value) => console.log(value));
console.log('end');start
observable is running
a
endYou can also defer Promise-based work by putting its creation inside a function and calling that function later. Laziness isn't something only Observables can provide.
Cancellation Depends on Teardown
RxJS gives you a subscription lifecycle, not automatic control over arbitrary work. This example actually cancels its timer because teardown calls clearTimeout().
import { Observable } from 'rxjs';
const observable = new Observable((observer) => {
const timer = setTimeout(() => {
console.log('calling next');
observer.next('a');
observer.complete();
}, 1000);
return () => {
clearTimeout(timer);
console.log('timer cleared');
};
});
const subscription = observable.subscribe((value) => console.log(value));
subscription.unsubscribe();
// Output: timer clearedWithout that teardown, the timer callback would still run and print calling next; its value wouldn't reach the unsubscribed consumer. Unsubscribing stops delivery, but stopping the source requires appropriate cleanup.
Native Promises have no cancellation method, but the underlying operation may be abortable. For example, fetch() accepts an AbortSignal; passing controller.signal and calling controller.abort() can abort a pending request. An Observable wrapping fetch needs equivalent abort-aware teardown to cancel that request. See MDN's Fetch guide.
Unicast vs Multicast: Cold and Shared Observables
Calling Observables “multicast” by default is incorrect. A cold Observable creates a producer for each subscription; multicast means one producer serves multiple consumers. A hot source has its producer outside the individual subscription, such as an already-running event source.
Two handlers attached to the same Promise receive the same fulfillment value. This prints two lines containing the same random number, not one line.
const promise = new Promise((resolve) => {
resolve(Math.random());
});
promise.then((value) => console.log('A:', value));
promise.then((value) => console.log('B:', value));Here, each subscription invokes Math.random() independently, so the values can differ. That is cold, unicast execution—not multicasting.
import { Observable } from 'rxjs';
const observable = new Observable((observer) => {
observer.next(Math.random());
observer.complete();
});
observable.subscribe((value) => console.log('A:', value));
observable.subscribe((value) => console.log('B:', value));Use share() to distribute one active source execution to overlapping subscribers. The delayed emission below gives both subscribers time to join that execution.
import { Observable, share } from 'rxjs';
let executions = 0;
const cold = new Observable((observer) => {
const run = ++executions;
console.log('producer', run);
const timer = setTimeout(() => {
observer.next(run);
observer.complete();
}, 100);
return () => clearTimeout(timer);
});
const shared = cold.pipe(share());
shared.subscribe((value) => console.log('A:', value));
shared.subscribe((value) => console.log('B:', value));producer 1
A: 1
B: 1By default, share() resets after completion, so a later subscription starts a new source execution. Two sequential subscriptions to a synchronously completing source therefore won't share that execution. For late subscribers that need previous values, consider shareReplay(); choose its replay buffer and reference-counting behavior deliberately. A Subject is another mechanism for multicasting notifications.
Synchronous vs Asynchronous Handlers
Promise handlers run through the microtask queue, after the current synchronous code finishes. Observable delivery depends on the source and scheduler: neither subscribing nor using an Observable makes surrounding code wait for future events.
const promise = new Promise((resolve) => {
resolve('promise is resolving');
});
console.log('START');
promise.then((value) => console.log(value));
console.log('END');
// Output:
// START
// END
// promise is resolvingimport { Observable } from 'rxjs';
const observable = new Observable((observer) => {
observer.next('next being called');
observer.complete();
});
console.log('START');
observable.subscribe((value) => console.log(value));
console.log('END');
// Output:
// START
// next being called
// ENDThe second example delivers synchronously. The earlier timer-backed Observable delivers later, after the timer fires; both are Observables.
Promise vs RxJS: What Operators Add
RxJS is a library for composing asynchronous and event-based programs, not merely another name for Observable. Its operators let you build pipelines: filter() selects values, while map() transforms each selected value.
import { from, filter, map } from 'rxjs';
from([1, 2, 3, 4])
.pipe(
filter((value) => value % 2 === 0),
map((value) => value * 10),
)
.subscribe({
next: (value) => console.log(value),
error: (error) => console.error(error),
complete: () => console.log('done'),
});
// Output:
// 20
// 40
// donefrom() converts the array into successive notifications. More importantly for asynchronous applications, operators such as switchMap() coordinate inner streams: when a new source value arrives, it unsubscribes from the previous inner Observable and observes the new one. Actual cancellation still depends on that inner source's teardown.
Promises also compose: then() returns a new Promise that follows the handler's result. Use Promise chaining for a sequence of eventual results; use RxJS when the repeated-value and subscription model makes the workflow clearer.
When to Use a Promise or an Observable
- Choose a Promise for one result: a request, a file operation, or a sequential workflow. Prefer the API your platform already exposes, and use async/await when it improves readability.
- Choose an Observable for a continuing stream: repeated events, changing state, or coordinated asynchronous sources. Our guide to when to use an Observable explores that decision further.
- Don't add RxJS just to wrap one Promise: choose it when its stream operators and lifecycle management simplify the application.
Neither abstraction is universally better. Decide how many results you need, who owns the work, whether consumers share it, and how that work ends.
Common Mistakes
- Leaving finite sources open: after the last value, signal completion or failure rather than leaving the subscription active.
- Using completion as the only cleanup hook: early unsubscription doesn't call the observer's completion handler. Register resource cleanup as teardown.
- Duplicating work accidentally: subscribing twice to a cold source creates two producers. Share only when consumers should own one execution together.
- Assuming an Observable makes expensive code asynchronous: synchronous producer code still executes synchronously during subscription.
FAQ
What is the difference between a Promise and an Observable in JavaScript?
A Promise settles once with a fulfillment value or rejection reason. An RxJS Observable can deliver zero or more values, then complete or error, and provides a subscription lifecycle for cleanup.
When should I use an Observable instead of a Promise?
Choose an Observable when you need repeated values, composition across streams, or subscription-based resource management. For one eventual result, a Promise is usually the simpler choice.
Can a Promise be canceled?
A native Promise has no cancellation method. Its underlying operation may support cancellation through a separate API, such as passing an AbortSignal to fetch and aborting its controller.
Does unsubscribe cancel the underlying Observable operation?
Unsubscribing stops notifications to that subscriber and runs registered teardown. It cancels the underlying work only when cleanup supports that, such as clearing a timer or aborting a request; a shared producer may continue for other subscribers.
Are RxJS Observables cold or hot by default?
The Observable type alone doesn't determine whether a source is cold or hot. A source that creates a producer for each subscription is cold; a hot source has its producer outside the individual subscription, and sharing operators can distribute one execution among subscribers.
Further Reading
- MDN: Promise constructor — executor timing and resolution behavior.
- MDN: Using promises — chaining, errors, cancellation, and microtasks.
- RxJS: Observables — creation, notifications, and disposal.
- RxJS: Subscriptions — resource ownership and unsubscription.
- RxJS glossary and semantics — producers, cold and hot sources, and finalization.
- RxJS: share() — shared subscriptions and reset behavior.
- RxJS: shareReplay() — replaying values and reference counting.
- MDN: Web-platform Observable.subscribe() — the separate experimental browser API.
