frontend

Observables vs Promises in JavaScript

January 4, 2026

Observables vs Promises in JavaScript

Both Observables and Promises are used to handle asynchronous operations in JavaScript, but they serve different purposes and have distinct characteristics. Understanding their differences is crucial for choosing the right tool for your use case.

Key Differences

| Aspect | Promises | Observables | | ------------------ | -------------------------------------------- | -------------------------------------------------- | | Value Emission | Single value, one-time resolution | Multiple values over time, continuous emission | | Execution | Executes immediately on creation | Lazy execution, starts only on subscription | | Cancellation | Cannot be cancelled once started | Supports cancellation via unsubscribe() | | Transformation | Basic chaining with .then() and .catch() | Rich operator ecosystem (map, filter, merge, etc.) | | Error Handling | Single error, then terminates | Can handle errors and continue emitting | | Use Case | One-time async operations | Streams of data, events, real-time updates |

Promises

A Promise represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

Characteristics

  • Single Value: Resolves or rejects once
  • Eager Execution: Starts executing immediately when created
  • No Cancellation: Cannot be cancelled once started
  • Simple API: .then(), .catch(), .finally()

Example

// Promise Example
const promiseExample = () => {
  const promise = new Promise((resolve) => {
    setTimeout(() => resolve("Promise resolved!"), 1000);
  });

  promise.then(console.log); // "Promise resolved!" after 1 second
};

promiseExample();

Promise Chaining

fetch("/api/user")
  .then((response) => response.json())
  .then((user) => fetch(`/api/posts/${user.id}`))
  .then((response) => response.json())
  .then((posts) => console.log(posts))
  .catch((error) => console.error(error));

Observables

Observables are a pattern for handling asynchronous data streams. They're part of the RxJS library and are heavily used in Angular.

Characteristics

  • Multiple Values: Can emit multiple values over time
  • Lazy Execution: Only executes when subscribed to
  • Cancellable: Can be unsubscribed to cancel execution
  • Rich Operators: Extensive operator library for transformation

Example

// Observable Example (using RxJS)
const { Observable } = require("rxjs");

const observableExample = () => {
  const observable = new Observable((subscriber) => {
    let count = 0;
    const intervalId = setInterval(() => {
      subscriber.next(`Observable value: ${count++}`);
    }, 1000);

    // Cleanup function
    return () => {
      clearInterval(intervalId);
      console.log("Observable cleaned up");
    };
  });

  const subscription = observable.subscribe(console.log);

  // Unsubscribe after 5 seconds
  setTimeout(() => {
    subscription.unsubscribe();
    console.log("Observable unsubscribed");
  }, 5000);
};

observableExample();

Detailed Comparison

1. Value Emission

Promises: Emit a single value

const promise = new Promise((resolve) => {
  resolve("Single value");
});

promise.then((value) => console.log(value)); // "Single value"
// That's it - promise is done

Observables: Can emit multiple values

const observable = new Observable((subscriber) => {
  subscriber.next("First value");
  subscriber.next("Second value");
  subscriber.next("Third value");
  subscriber.complete();
});

observable.subscribe((value) => console.log(value));
// "First value"
// "Second value"
// "Third value"

2. Execution Timing

Promises: Execute immediately

console.log("Before promise");
const promise = new Promise((resolve) => {
  console.log("Promise executing"); // Runs immediately
  resolve("Done");
});
console.log("After promise");
// Output:
// "Before promise"
// "Promise executing"
// "After promise"

Observables: Lazy execution (only when subscribed)

console.log("Before observable");
const observable = new Observable((subscriber) => {
  console.log("Observable executing"); // Doesn't run yet
  subscriber.next("Value");
});
console.log("After observable");
// Output:
// "Before observable"
// "After observable"
// (Nothing happens until subscription)

observable.subscribe(); // Now it executes
// "Observable executing"

3. Cancellation

Promises: Cannot be cancelled

const promise = fetch("/api/data");
// Once started, cannot stop it
promise.then((data) => console.log(data));
// No way to cancel if user navigates away

Observables: Can be cancelled

const observable = new Observable((subscriber) => {
  const intervalId = setInterval(() => {
    subscriber.next("Tick");
  }, 1000);

  return () => clearInterval(intervalId); // Cleanup
});

const subscription = observable.subscribe(console.log);

// Later, cancel it
subscription.unsubscribe(); // Stops execution and cleans up

4. Transformation Capabilities

Promises: Basic chaining

fetch("/api/data")
  .then((response) => response.json())
  .then((data) => data.filter((item) => item.active))
  .then((activeItems) => activeItems.map((item) => item.name))
  .catch((error) => console.error(error));

Observables: Rich operators

import { from, map, filter, debounceTime, mergeMap } from "rxjs";

from(fetch("/api/data"))
  .pipe(
    mergeMap((response) => response.json()),
    filter((item) => item.active),
    debounceTime(300),
    map((item) => item.name)
  )
  .subscribe(console.log);

Use Cases

When to Use Promises

One-time async operations

  • API calls that return a single response
  • File reading/writing
  • Database queries
  • Simple async/await scenarios
// Perfect for Promises
async function getUserData(userId) {
  const user = await fetch(`/api/users/${userId}`).then((r) => r.json());
  const posts = await fetch(`/api/posts/${userId}`).then((r) => r.json());
  return { user, posts };
}

When to Use Observables

Streams of data

  • Real-time data (WebSocket connections)
  • User input events (keyboard, mouse)
  • Time-based sequences (intervals, timers)
  • Multiple values over time
  • Complex event handling
// Perfect for Observables
import { fromEvent, debounceTime, map } from "rxjs";

const searchInput = document.getElementById("search");
fromEvent(searchInput, "input")
  .pipe(
    debounceTime(300),
    map((event) => event.target.value),
    mergeMap((query) => fetch(`/api/search?q=${query}`))
  )
  .subscribe((results) => displayResults(results));

Converting Between Promises and Observables

Promise to Observable

import { from } from "rxjs";

const promise = fetch("/api/data").then((r) => r.json());
const observable = from(promise);

observable.subscribe((data) => console.log(data));

Observable to Promise

import { firstValueFrom } from "rxjs";

const observable = new Observable((subscriber) => {
  subscriber.next("value");
  subscriber.complete();
});

const promise = firstValueFrom(observable);
promise.then((value) => console.log(value));

Why Angular Uses Observables

Angular heavily uses Observables (via RxJS) because:

  1. HTTP Client: Returns Observables for HTTP requests
  2. Reactive Forms: Form value changes are Observables
  3. Router Events: Navigation events are Observables
  4. Real-time Updates: Perfect for WebSocket connections
  5. Event Handling: User interactions can be treated as streams
// Angular example
import { Component } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Component({...})
export class UserComponent {
  constructor(private http: HttpClient) {}

  // HTTP returns Observable
  users$ = this.http.get('/api/users');

  // Can chain operators
  activeUsers$ = this.users$.pipe(
    map(users => users.filter(u => u.active))
  );
}

Common Observable Operators

import { map, filter, debounceTime, mergeMap, switchMap, take } from "rxjs";

observable
  .pipe(
    map((x) => x * 2), // Transform values
    filter((x) => x > 10), // Filter values
    debounceTime(300), // Wait for pause
    mergeMap((x) => fetch(x)), // Flatten inner observables
    take(5) // Take first 5 values
  )
  .subscribe(console.log);

Error Handling

Promises

promise
  .then((data) => process(data))
  .catch((error) => {
    console.error(error);
    // Promise chain ends here
  });

Observables

observable
  .pipe(
    map((data) => process(data)),
    catchError((error) => {
      console.error(error);
      return of(defaultValue); // Can continue with default value
    })
  )
  .subscribe(console.log);

Summary

  • Promises: Best for single async operations, simple error handling, and when you need immediate execution
  • Observables: Best for streams of data, real-time updates, complex transformations, and when you need cancellation

Choose Promises for simple, one-time async operations. Choose Observables for reactive programming, event streams, and complex data transformations. Many modern frameworks (like Angular) use Observables extensively, while others (like React) primarily use Promises with async/await.