Skip to main content

Overview

The observeOn operator re-emits all notifications (next, error, and complete) from the source Observable using a specified scheduler. This allows you to control when and how notifications are delivered to observers.
Unlike subscribeOn (which affects when subscription happens), observeOn affects when notifications are delivered to subscribers.

Signature

function observeOn<T>(
  scheduler: SchedulerLike,
  delay: number = 0
): MonoTypeOperatorFunction<T>

Parameters

scheduler
SchedulerLike
required
The scheduler that will be used to reschedule notifications from the source Observable. Common schedulers:
  • asyncScheduler - Uses setTimeout
  • asapScheduler - Uses microtasks (Promise.resolve)
  • queueScheduler - Synchronous queue
  • animationFrameScheduler - Uses requestAnimationFrame
delay
number
default:"0"
Number of milliseconds to delay each notification. This is in addition to the scheduler’s natural timing.

Returns

return
MonoTypeOperatorFunction<T>
A function that returns an Observable that emits the same notifications as the source, but rescheduled with the provided scheduler.

Usage Examples

Smooth Browser Animations

Ensure values are emitted just before browser repaint:
import { interval, observeOn, animationFrameScheduler } from 'rxjs';

const someDiv = document.createElement('div');
someDiv.style.cssText = 'width: 200px; background: #09c';
document.body.appendChild(someDiv);

const intervals = interval(10); // Default async scheduler

intervals.pipe(
  observeOn(animationFrameScheduler) // Observe on animation frame
).subscribe(val => {
  someDiv.style.height = val + 'px';
});
// Ensures smooth 60fps animation

Delayed Notifications

Add delay to all notifications including errors:
import { of, observeOn, asyncScheduler, concat, throwError } from 'rxjs';

concat(
  of(1, 2, 3),
  throwError(() => new Error('Error!'))
).pipe(
  observeOn(asyncScheduler, 1000)
).subscribe({
  next: val => console.log(`${Date.now()}: ${val}`),
  error: err => console.log(`${Date.now()}: ${err.message}`)
});

// All notifications delayed by 1000ms, including error

Testing with Virtual Scheduler

Control timing in tests:
import { of, observeOn } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';

const testScheduler = new TestScheduler((actual, expected) => {
  expect(actual).toEqual(expected);
});

testScheduler.run(({ expectObservable }) => {
  const source$ = of(1, 2, 3).pipe(
    observeOn(testScheduler)
  );
  
  expectObservable(source$).toBe('(abc|)', { a: 1, b: 2, c: 3 });
});

Processing in Batches

Prevent blocking the main thread:
import { from, observeOn, asyncScheduler } from 'rxjs';
import { map } from 'rxjs/operators';

const hugeArray = Array.from({ length: 1000000 }, (_, i) => i);

from(hugeArray).pipe(
  map(x => heavyComputation(x)),
  observeOn(asyncScheduler) // Reschedule after each item
).subscribe(result => {
  updateUI(result);
  // UI remains responsive
});

function heavyComputation(x: number): number {
  // Expensive operation
  return x * x;
}

How It Works

observeOn schedules each notification (next, error, complete) on the specified scheduler:
export function observeOn<T>(scheduler: SchedulerLike, delay = 0): MonoTypeOperatorFunction<T> {
  return (source) =>
    new Observable((destination) => {
      source.subscribe(
        operate({
          destination,
          next: (value) => executeSchedule(destination, scheduler, () => destination.next(value), delay),
          error: (err) => executeSchedule(destination, scheduler, () => destination.error(err), delay),
          complete: () => executeSchedule(destination, scheduler, () => destination.complete(), delay),
        })
      );
    });
}
Each notification is wrapped in a scheduled task, ensuring it’s delivered according to the scheduler’s timing.

Difference: observeOn vs delay

Key difference: observeOn delays all notifications including errors, while delay passes errors through immediately.
FeatureobserveOndelay
Delays next
Delays complete❌ (immediate)
Delays error❌ (immediate)
PurposeControl schedulerAdd time delay
Use caseExecution contextTime shifting
import { of, throwError, concat, observeOn, delay, asyncScheduler } from 'rxjs';

// With observeOn - error is delayed
concat(
  of(1),
  throwError(() => new Error('Oops'))
).pipe(
  observeOn(asyncScheduler, 1000)
).subscribe({
  next: console.log,
  error: err => console.log('Error after 1s:', err.message)
});

// With delay - error is immediate  
concat(
  of(1),
  throwError(() => new Error('Oops'))
).pipe(
  delay(1000)
).subscribe({
  next: console.log,
  error: err => console.log('Error immediately:', err.message)
});

Common Use Cases

  1. Smooth Animations: Use animationFrameScheduler for 60fps animations
  2. UI Responsiveness: Prevent blocking with asyncScheduler
  3. Debouncing Work: Control when expensive operations run
  4. Testing: Use TestScheduler for deterministic timing
  5. Event Loop Control: Manage execution order and timing

Scheduler Comparison

Choose the right scheduler for your use case:
  • animationFrameScheduler: Browser animations (60fps)
  • asapScheduler: Microtask queue (fastest async)
  • asyncScheduler: Macro task queue (setTimeout)
  • queueScheduler: Synchronous queue (testing)
SchedulerTimingUse Case
animationFrameSchedulerBefore browser repaint (~16ms)Animations, visual updates
asapSchedulerMicrotask queue (Promise)Defer to next microtask
asyncSchedulerMacrotask queue (setTimeout)General async operations
queueSchedulerSynchronousRecursive operations, testing

Performance Considerations

Using observeOn on high-frequency Observables creates many scheduled tasks. Consider throttling or buffering first.
  • Each notification creates a scheduled task
  • For high-frequency sources (e.g., mouse move), consider throttleTime or auditTime first
  • Schedulers add overhead - use only when needed
  • Animation frame scheduler is optimized for 60fps

Best Practices

  1. Place strategically: Put observeOn where you need to change execution context
  2. Combine with throttling: For high-frequency events, throttle before observeOn
  3. Use appropriate scheduler: Match scheduler to use case
  4. Avoid overuse: Only reschedule when necessary
  5. Consider delay operator: If you only need time delay (not scheduler control), use delay

See Also