Back to Blog

Understanding JavaScript Promise Execution Order

Vidhya Sagar ThakurSeptember 20, 2026128 min read0 views

Promise execution order is one of the most confusing aspects of JavaScript's asynchronous model. Even experienced engineers struggle to predict the exact sequence when multiple Promises, callbacks, and async operations interleave. This deep dive explores the internal mechanics that determine execution order, the spec-mandated algorithms that engines follow, and the production pitfalls that emerge when assumptions about ordering prove wrong.

Scale Context

DimensionProduction Scale
Active Promise chains1,000-1,000,000
Promise.all batch size10-10,000
Nested .then() depth1-50
Promise creations/second10,000-1,000,000
Rejection rate0.1%-5%
Race conditions from ordering1-100/day
Unhandled rejection errors10-1,000/day
Promise memory overhead40-120 bytes each
Microtask queue depth100-100,000
Promise resolution latency<1ms-100ms
Close Up Hand GIF by Alex BoyaGIF via GIPHY

High-Level Architecture

text
┌─────────────────────────────────────────────────────────────────────────────┐
│                       Promise State Machine                                  │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│    ┌─────────────────────────────────────────────────────────────────────┐  │
│    │                         PENDING                                      │  │
│    │                                                                      │  │
│    │  • Reactions queued but not executed                                │  │
│    │  • Value/reason not yet determined                                  │  │
│    │  • Can transition to FULFILLED or REJECTED                          │  │
│    │                                                                      │  │
│    └───────────────────────┬───────────────────────┬─────────────────────┘  │
│                            │                       │                         │
│              resolve(value)│                       │reject(reason)           │
│                            │                       │                         │
│                            ▼                       ▼                         │
│    ┌──────────────────────────────┐  ┌──────────────────────────────────┐  │
│    │         FULFILLED            │  │          REJECTED                 │  │
│    │                              │  │                                   │  │
│    │  • Value is settled          │  │  • Reason is settled              │  │
│    │  • .then() handlers queued   │  │  • .catch() handlers queued       │  │
│    │    as microtasks             │  │    as microtasks                  │  │
│    │  • State is immutable        │  │  • State is immutable             │  │
│    │                              │  │                                   │  │
│    └──────────────────────────────┘  └──────────────────────────────────┘  │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│                    Promise Reaction Job Flow                                 │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  Promise A                    Promise B (.then result)                      │
│  ┌─────────┐                  ┌─────────┐                                   │
│  │ settled │                  │ pending │                                   │
│  │ value:5 │                  │         │                                   │
│  └────┬────┘                  └────▲────┘                                   │
│       │                            │                                        │
│       │  A.then(handler)           │                                        │
│       │                            │                                        │
│       ▼                            │                                        │
│  ┌────────────────────────────────────────────────────────────────────┐    │
│  │                       Microtask Queue                               │    │
│  │                                                                     │    │
│  │  ┌─────────────────────────────────────────────────────────────┐  │    │
│  │  │ PromiseReactionJob                                          │  │    │
│  │  │                                                              │  │    │
│  │  │  1. Get handler from reaction                                │  │    │
│  │  │  2. Call handler with value (5)                             │  │    │
│  │  │  3. If handler returns: resolve Promise B with return value │  │    │
│  │  │  4. If handler throws: reject Promise B with error          │  │    │
│  │  │  5. If handler returns Promise: adopt its state             │  │    │
│  │  │                                                              │  │    │
│  │  └─────────────────────────────────────────────────────────────┘  │    │
│  │                                                                     │    │
│  └────────────────────────────────────────────────────────────────────┘    │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘
Celebrate High Level GIF by NeighborlyNotary®GIF via GIPHY

Promise Internal State

TypeScript
// Internal Promise representation (spec-aligned)
interface PromiseInternals<T> {
  // [[PromiseState]]: "pending" | "fulfilled" | "rejected"
  state: PromiseState;

  // [[PromiseResult]]: the value or reason
  result: T | unknown | undefined;

  // [[PromiseFulfillReactions]]: list of reactions for fulfillment
  fulfillReactions: PromiseReaction<T>[];

  // [[PromiseRejectReactions]]: list of reactions for rejection
  rejectReactions: PromiseReaction<T>[];

  // [[PromiseIsHandled]]: whether rejection has been observed
  isHandled: boolean;
}

type PromiseState = 'pending' | 'fulfilled' | 'rejected';

// Reaction records - queued handlers
interface PromiseReaction<T> {
  // [[Capability]]: The promise capability for the derived promise
  capability: PromiseCapability<unknown>;

  // [[Type]]: "Fulfill" or "Reject"
  type: 'fulfill' | 'reject';

  // [[Handler]]: The handler function or undefined
  handler: ((value: T) => unknown) | undefined;
}

// Promise capability - represents a deferred promise
interface PromiseCapability<T> {
  promise: Promise<T>;
  resolve: (value: T | PromiseLike<T>) => void;
  reject: (reason: unknown) => void;
}

// Full Promise implementation following spec
class PromiseImpl<T> {
  private internal: PromiseInternals<T>;

  constructor(executor: (
    resolve: (value: T | PromiseLike<T>) => void,
    reject: (reason: unknown) => void
  ) => void) {
    // Initialize internal slots
    this.internal = {
      state: 'pending',
      result: undefined,
      fulfillReactions: [],
      rejectReactions: [],
      isHandled: false,
    };

    // Create resolving functions
    const { resolve, reject } = this.createResolvingFunctions();

    // Execute the executor synchronously
    try {
      executor(resolve, reject);
    } catch (error) {
      // If executor throws, reject the promise
      reject(error);
    }
  }

  private createResolvingFunctions(): {
    resolve: (value: T | PromiseLike<T>) => void;
    reject: (reason: unknown) => void;
  } {
    let alreadyResolved = false;

    const resolve = (value: T | PromiseLike<T>): void => {
      if (alreadyResolved) return;
      alreadyResolved = true;

      // If resolving with self, reject with TypeError
      if (value === (this as unknown)) {
        this.rejectPromise(new TypeError('Cannot resolve promise with itself'));
        return;
      }

      // If value is a thenable, adopt its state
      if (this.isThenable(value)) {
        this.resolveWithThenable(value as PromiseLike<T>);
        return;
      }

      // Otherwise, fulfill with the value
      this.fulfillPromise(value as T);
    };

    const reject = (reason: unknown): void => {
      if (alreadyResolved) return;
      alreadyResolved = true;
      this.rejectPromise(reason);
    };

    return { resolve, reject };
  }

  private isThenable(value: unknown): value is PromiseLike<unknown> {
    if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
      return false;
    }
    return typeof (value as any).then === 'function';
  }

  private resolveWithThenable(thenable: PromiseLike<T>): void {
    // Create a PromiseResolveThenableJob and enqueue it
    const job = (): void => {
      const { resolve, reject } = this.createResolvingFunctions();

      try {
        thenable.then(resolve, reject);
      } catch (error) {
        reject(error);
      }
    };

    // IMPORTANT: This is a separate job, not a reaction job
    // It goes to the microtask queue
    queueMicrotask(job);
  }

  private fulfillPromise(value: T): void {
    if (this.internal.state !== 'pending') return;

    // Capture reactions before clearing
    const reactions = this.internal.fulfillReactions;

    // Update state
    this.internal.state = 'fulfilled';
    this.internal.result = value;
    this.internal.fulfillReactions = [];
    this.internal.rejectReactions = [];

    // Trigger reactions
    this.triggerReactions(reactions, value);
  }

  private rejectPromise(reason: unknown): void {
    if (this.internal.state !== 'pending') return;

    const reactions = this.internal.rejectReactions;

    this.internal.state = 'rejected';
    this.internal.result = reason;
    this.internal.fulfillReactions = [];
    this.internal.rejectReactions = [];

    // Track unhandled rejection
    if (!this.internal.isHandled) {
      this.trackUnhandledRejection(reason);
    }

    this.triggerReactions(reactions, reason);
  }

  private triggerReactions(
    reactions: PromiseReaction<T>[],
    value: unknown
  ): void {
    for (const reaction of reactions) {
      // Queue each reaction as a microtask
      const job = this.createReactionJob(reaction, value);
      queueMicrotask(job);
    }
  }

  private createReactionJob(
    reaction: PromiseReaction<T>,
    value: unknown
  ): () => void {
    return () => {
      const { capability, handler, type } = reaction;

      if (handler === undefined) {
        // No handler - propagate value/reason
        if (type === 'fulfill') {
          capability.resolve(value as any);
        } else {
          capability.reject(value);
        }
        return;
      }

      // Execute handler
      let result: unknown;
      try {
        result = handler(value as T);
        capability.resolve(result as any);
      } catch (error) {
        capability.reject(error);
      }
    };
  }

  then<TResult1 = T, TResult2 = never>(
    onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
    onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null
  ): PromiseImpl<TResult1 | TResult2> {
    // Mark as handled to prevent unhandled rejection warnings
    this.internal.isHandled = true;

    // Create capability for the derived promise
    const capability = this.newPromiseCapability<TResult1 | TResult2>();

    // Create fulfill reaction
    const fulfillReaction: PromiseReaction<T> = {
      capability,
      type: 'fulfill',
      handler: typeof onFulfilled === 'function' ? onFulfilled : undefined,
    };

    // Create reject reaction
    const rejectReaction: PromiseReaction<T> = {
      capability,
      type: 'reject',
      handler: typeof onRejected === 'function' ? onRejected : undefined,
    };

    // Handle based on current state
    if (this.internal.state === 'pending') {
      // Queue reactions for later
      this.internal.fulfillReactions.push(fulfillReaction);
      this.internal.rejectReactions.push(rejectReaction);
    } else if (this.internal.state === 'fulfilled') {
      // Queue fulfill reaction as microtask
      const job = this.createReactionJob(fulfillReaction, this.internal.result);
      queueMicrotask(job);
    } else {
      // Queue reject reaction as microtask
      const job = this.createReactionJob(rejectReaction, this.internal.result);
      queueMicrotask(job);
    }

    return capability.promise;
  }

  catch<TResult = never>(
    onRejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null
  ): PromiseImpl<T | TResult> {
    return this.then(undefined, onRejected);
  }

  finally(onFinally?: (() => void) | null): PromiseImpl<T> {
    const thenFinally = typeof onFinally === 'function'
      ? (value: T): T | PromiseLike<T> => {
          const result = onFinally();
          if (this.isThenable(result)) {
            return (result as PromiseLike<unknown>).then(() => value);
          }
          return value;
        }
      : undefined;

    const catchFinally = typeof onFinally === 'function'
      ? (reason: unknown): never => {
          const result = onFinally();
          if (this.isThenable(result)) {
            return (result as PromiseLike<unknown>).then(() => {
              throw reason;
            }) as never;
          }
          throw reason;
        }
      : undefined;

    return this.then(thenFinally, catchFinally) as PromiseImpl<T>;
  }

  private newPromiseCapability<U>(): PromiseCapability<U> {
    let resolve!: (value: U | PromiseLike<U>) => void;
    let reject!: (reason: unknown) => void;

    const promise = new PromiseImpl<U>((res, rej) => {
      resolve = res;
      reject = rej;
    });

    return { promise, resolve, reject };
  }

  private trackUnhandledRejection(reason: unknown): void {
    // Queue a check for unhandled rejection
    queueMicrotask(() => {
      if (!this.internal.isHandled) {
        console.error('Unhandled promise rejection:', reason);
        // In browsers: dispatch 'unhandledrejection' event
        // In Node.js: emit 'unhandledRejection' event
      }
    });
  }

  // Static methods
  static resolve<T>(value: T | PromiseLike<T>): PromiseImpl<T> {
    if (value instanceof PromiseImpl) {
      return value;
    }
    return new PromiseImpl(resolve => resolve(value));
  }

  static reject<T = never>(reason: unknown): PromiseImpl<T> {
    return new PromiseImpl((_, reject) => reject(reason));
  }
}
software product GIFGIF via GIPHY

Execution Order Rules

TypeScript
// Rule-based execution order predictor
class ExecutionOrderPredictor {
  private executionLog: string[] = [];
  private ruleViolations: RuleViolation[] = [];

  // Rule 1: Synchronous code runs first
  demonstrateRule1(): void {
    this.log('1. sync start');

    Promise.resolve().then(() => {
      this.log('3. promise then');
    });

    this.log('2. sync end');

    // Output: 1, 2, 3
    // Rule: All synchronous code completes before any microtask
  }

  // Rule 2: Promise.resolve().then() is always async
  demonstrateRule2(): void {
    this.log('1. sync start');

    // Even though Promise.resolve() creates an already-fulfilled promise,
    // .then() handler is ALWAYS queued as a microtask
    Promise.resolve('immediate').then(value => {
      this.log(`3. ${value}`);
    });

    this.log('2. sync end');

    // Output: 1, 2, 3
    // Rule: .then() handlers NEVER run synchronously
  }

  // Rule 3: Microtasks are FIFO within the same checkpoint
  demonstrateRule3(): void {
    this.log('1. sync');

    Promise.resolve().then(() => this.log('2. first then'));
    Promise.resolve().then(() => this.log('3. second then'));
    Promise.resolve().then(() => this.log('4. third then'));

    queueMicrotask(() => this.log('5. queueMicrotask'));

    // Output: 1, 2, 3, 4, 5
    // Rule: Microtasks execute in the order they were queued
  }

  // Rule 4: Chained .then() creates sequential microtasks
  demonstrateRule4(): void {
    this.log('1. sync');

    Promise.resolve()
      .then(() => this.log('3. first in chain'))
      .then(() => this.log('5. second in chain'))
      .then(() => this.log('6. third in chain'));

    Promise.resolve()
      .then(() => this.log('4. separate chain'));

    this.log('2. sync end');

    // Output: 1, 2, 3, 4, 5, 6
    // Rule: Each .then() in a chain queues the NEXT one after executing
  }

  // Rule 5: Returning a Promise from .then() creates an extra microtask
  demonstrateRule5(): void {
    this.log('1. sync');

    Promise.resolve()
      .then(() => {
        this.log('3. outer then');
        return Promise.resolve('inner');
      })
      .then(value => {
        this.log(`6. after inner: ${value}`);
      });

    Promise.resolve().then(() => this.log('4. parallel then 1'));
    Promise.resolve().then(() => this.log('5. parallel then 2'));

    this.log('2. sync end');

    // Output: 1, 2, 3, 4, 5, 6
    // Rule: Returning Promise causes PromiseResolveThenableJob (extra microtask)
  }

  // Rule 6: await creates microtask boundary
  async demonstrateRule6(): Promise<void> {
    this.log('1. async start');

    const value = await Promise.resolve('awaited');
    this.log(`3. after await: ${value}`);

    this.log('4. async end');
  }

  // Call site:
  // this.log('0. before call');
  // this.demonstrateRule6();
  // this.log('2. after call');
  // Output: 0, 1, 2, 3, 4
  // Rule: await suspends and queues continuation as microtask

  // Rule 7: Multiple awaits create sequential microtasks
  async demonstrateRule7(): Promise<void> {
    this.log('start');

    await Promise.resolve(); // microtask 1
    this.log('after first await');

    await Promise.resolve(); // microtask 2
    this.log('after second await');

    await Promise.resolve(); // microtask 3
    this.log('after third await');
  }

  // Rule 8: Promise.all resolves when all promises resolve
  async demonstrateRule8(): Promise<void> {
    this.log('1. sync');

    Promise.all([
      Promise.resolve().then(() => this.log('3. all[0]')),
      Promise.resolve().then(() => this.log('4. all[1]')),
      Promise.resolve().then(() => this.log('5. all[2]')),
    ]).then(() => this.log('7. all complete'));

    Promise.resolve().then(() => this.log('6. parallel'));

    this.log('2. sync end');

    // Output: 1, 2, 3, 4, 5, 6, 7
    // Rule: Promise.all completion queued after all input promises resolve
  }

  // Rule 9: Promise.race resolves with first settled promise
  async demonstrateRule9(): Promise<void> {
    this.log('1. sync');

    const p1 = new Promise<string>(resolve =>
      setTimeout(() => resolve('slow'), 100)
    );
    const p2 = Promise.resolve('fast');

    Promise.race([p1, p2]).then(value => {
      this.log(`3. race winner: ${value}`);
    });

    this.log('2. sync end');

    // Output: 1, 2, 3 (with value 'fast')
    // Rule: First settled promise triggers race resolution
  }

  // Rule 10: Rejection propagates until caught
  demonstrateRule10(): void {
    this.log('1. sync');

    Promise.reject(new Error('failure'))
      .then(() => this.log('never runs'))
      .then(() => this.log('also never runs'))
      .catch(err => this.log(`3. caught: ${err.message}`))
      .then(() => this.log('4. after catch'));

    this.log('2. sync end');

    // Output: 1, 2, 3, 4
    // Rule: Rejection skips .then() handlers until .catch()
  }

  private log(message: string): void {
    this.executionLog.push(message);
    console.log(message);
  }
}

interface RuleViolation {
  rule: number;
  expected: string;
  actual: string;
}
Execution Order RulesGIF via GIPHY

Complex Ordering Scenarios

TypeScript
// Advanced scenarios that commonly cause confusion
class ComplexOrderingScenarios {
  // Scenario 1: Nested Promise resolution timing
  async nestedPromiseTiming(): Promise<void> {
    console.log('A');

    new Promise<void>(resolve => {
      console.log('B');
      resolve();
    }).then(() => {
      console.log('C');
    });

    new Promise<void>(resolve => {
      console.log('D');
      resolve();
      console.log('E');
    }).then(() => {
      console.log('F');
    });

    console.log('G');

    // Output: A, B, D, E, G, C, F
    // Why: Executor runs sync, .then() is async
  }

  // Scenario 2: Promise returning Promise
  async promiseReturningPromise(): Promise<void> {
    console.log('1');

    Promise.resolve()
      .then(() => {
        console.log('2');
        return Promise.resolve();
      })
      .then(() => {
        console.log('5');
      });

    Promise.resolve()
      .then(() => console.log('3'))
      .then(() => console.log('4'));

    // Output: 1, 2, 3, 4, 5
    // Why: Returning Promise.resolve() adds extra microtask tick
  }

  // Scenario 3: await vs .then() interleaving
  async awaitVsThenInterleaving(): Promise<void> {
    const asyncFn = async () => {
      console.log('A1');
      await Promise.resolve();
      console.log('A2');
      await Promise.resolve();
      console.log('A3');
    };

    console.log('S1');

    asyncFn();

    Promise.resolve()
      .then(() => console.log('P1'))
      .then(() => console.log('P2'))
      .then(() => console.log('P3'));

    console.log('S2');

    // Output: S1, A1, S2, A2, P1, A3, P2, P3
    // Why: await and .then() chains interleave in microtask queue
  }

  // Scenario 4: Promise.all with async mapping
  async promiseAllAsyncMap(): Promise<void> {
    const items = [1, 2, 3];

    console.log('start');

    await Promise.all(
      items.map(async (item) => {
        console.log(`begin ${item}`);
        await Promise.resolve();
        console.log(`end ${item}`);
      })
    );

    console.log('done');

    // Output: start, begin 1, begin 2, begin 3, end 1, end 2, end 3, done
    // Why: All async functions start sync until first await, then interleave
  }

  // Scenario 5: Mixed setTimeout and Promise
  mixedMacroMicro(): void {
    console.log('1');

    setTimeout(() => {
      console.log('2');
      Promise.resolve().then(() => console.log('3'));
      console.log('4');
    }, 0);

    Promise.resolve().then(() => {
      console.log('5');
      setTimeout(() => console.log('6'), 0);
      console.log('7');
    });

    console.log('8');

    // Output: 1, 8, 5, 7, 2, 4, 3, 6
    // Why: Microtasks drain before macrotasks; new microtasks in same checkpoint
  }

  // Scenario 6: Promise.resolve vs new Promise
  resolveVsNew(): void {
    console.log('1');

    Promise.resolve().then(() => console.log('2'));

    new Promise<void>(resolve => {
      console.log('3');
      resolve();
    }).then(() => console.log('4'));

    Promise.resolve().then(() => console.log('5'));

    console.log('6');

    // Output: 1, 3, 6, 2, 4, 5
    // Why: new Promise executor runs sync; all .then() handlers queue in order
  }

  // Scenario 7: Multiple await in sequence vs parallel
  async sequentialVsParallel(): Promise<void> {
    const delay = (ms: number, label: string) =>
      new Promise<string>(resolve =>
        setTimeout(() => {
          console.log(`resolved: ${label}`);
          resolve(label);
        }, ms)
      );

    // Sequential - each await waits for previous
    console.log('sequential start');
    const s1 = await delay(100, 's1');
    const s2 = await delay(100, 's2');
    console.log(`sequential done: ${s1}, ${s2}`);

    // Parallel - all start immediately
    console.log('parallel start');
    const [p1, p2] = await Promise.all([
      delay(100, 'p1'),
      delay(100, 'p2'),
    ]);
    console.log(`parallel done: ${p1}, ${p2}`);

    // Sequential: ~200ms total
    // Parallel: ~100ms total
  }

  // Scenario 8: Error propagation timing
  errorTiming(): void {
    console.log('1');

    Promise.resolve()
      .then(() => {
        console.log('2');
        throw new Error('fail');
      })
      .then(() => console.log('3 - skipped'))
      .catch(() => console.log('4 - caught'))
      .then(() => console.log('5 - recovery'));

    Promise.resolve()
      .then(() => console.log('6'))
      .then(() => console.log('7'));

    console.log('8');

    // Output: 1, 8, 2, 6, 4, 7, 5
    // Why: Error skips .then(), catch handles, interleaves with parallel chain
  }

  // Scenario 9: Dynamic Promise creation
  dynamicPromises(): void {
    console.log('1');

    const createChain = (depth: number, label: string): Promise<void> => {
      if (depth === 0) return Promise.resolve();
      return Promise.resolve().then(() => {
        console.log(`${label}-${depth}`);
        return createChain(depth - 1, label);
      });
    };

    createChain(3, 'A');
    createChain(3, 'B');

    console.log('2');

    // Output: 1, 2, A-3, B-3, A-2, B-2, A-1, B-1
    // Why: Each depth level interleaves between chains
  }

  // Scenario 10: finally() timing
  finallyTiming(): void {
    console.log('1');

    Promise.resolve('value')
      .then(v => {
        console.log(`2: ${v}`);
        return 'modified';
      })
      .finally(() => {
        console.log('3: finally');
        return 'ignored'; // Return value ignored
      })
      .then(v => {
        console.log(`4: ${v}`); // Gets 'modified', not 'ignored'
      });

    Promise.resolve().then(() => console.log('5'));

    console.log('6');

    // Output: 1, 6, 2: value, 5, 3: finally, 4: modified
    // Why: finally() creates additional microtask tick
  }
}
bangtan boys v GIFGIF via GIPHY

Promise Combinator Internals

TypeScript
// Internal implementation of Promise combinators
class PromiseCombinators {
  // Promise.all implementation
  static all<T>(promises: Iterable<T | PromiseLike<T>>): Promise<T[]> {
    return new Promise((resolve, reject) => {
      const promiseArray = Array.from(promises);
      const results: T[] = new Array(promiseArray.length);
      let remainingCount = promiseArray.length;
      let rejected = false;

      if (remainingCount === 0) {
        resolve(results);
        return;
      }

      promiseArray.forEach((promise, index) => {
        // Wrap in Promise.resolve to handle non-Promise values
        Promise.resolve(promise).then(
          value => {
            if (rejected) return;

            results[index] = value;
            remainingCount--;

            if (remainingCount === 0) {
              resolve(results);
            }
          },
          reason => {
            if (rejected) return;
            rejected = true;
            reject(reason);
          }
        );
      });
    });
  }

  // Promise.allSettled implementation
  static allSettled<T>(
    promises: Iterable<T | PromiseLike<T>>
  ): Promise<PromiseSettledResult<T>[]> {
    return new Promise(resolve => {
      const promiseArray = Array.from(promises);
      const results: PromiseSettledResult<T>[] = new Array(promiseArray.length);
      let remainingCount = promiseArray.length;

      if (remainingCount === 0) {
        resolve(results);
        return;
      }

      promiseArray.forEach((promise, index) => {
        Promise.resolve(promise).then(
          value => {
            results[index] = { status: 'fulfilled', value };
            if (--remainingCount === 0) resolve(results);
          },
          reason => {
            results[index] = { status: 'rejected', reason };
            if (--remainingCount === 0) resolve(results);
          }
        );
      });
    });
  }

  // Promise.race implementation
  static race<T>(promises: Iterable<T | PromiseLike<T>>): Promise<T> {
    return new Promise((resolve, reject) => {
      for (const promise of promises) {
        // First one to settle wins
        Promise.resolve(promise).then(resolve, reject);
      }
    });
  }

  // Promise.any implementation (ES2021)
  static any<T>(promises: Iterable<T | PromiseLike<T>>): Promise<T> {
    return new Promise((resolve, reject) => {
      const promiseArray = Array.from(promises);
      const errors: unknown[] = new Array(promiseArray.length);
      let remainingCount = promiseArray.length;
      let resolved = false;

      if (remainingCount === 0) {
        reject(new AggregateError([], 'All promises were rejected'));
        return;
      }

      promiseArray.forEach((promise, index) => {
        Promise.resolve(promise).then(
          value => {
            if (resolved) return;
            resolved = true;
            resolve(value);
          },
          reason => {
            if (resolved) return;
            errors[index] = reason;
            if (--remainingCount === 0) {
              reject(new AggregateError(errors, 'All promises were rejected'));
            }
          }
        );
      });
    });
  }

  // Promise.withResolvers (ES2024)
  static withResolvers<T>(): {
    promise: Promise<T>;
    resolve: (value: T | PromiseLike<T>) => void;
    reject: (reason: unknown) => void;
  } {
    let resolve!: (value: T | PromiseLike<T>) => void;
    let reject!: (reason: unknown) => void;

    const promise = new Promise<T>((res, rej) => {
      resolve = res;
      reject = rej;
    });

    return { promise, resolve, reject };
  }
}

// Execution order with combinators
class CombinatorOrderingDemos {
  // Promise.all ordering
  async allOrdering(): Promise<void> {
    console.log('1. start');

    const results = await Promise.all([
      Promise.resolve().then(() => {
        console.log('3. first promise');
        return 'a';
      }),
      Promise.resolve().then(() => {
        console.log('4. second promise');
        return 'b';
      }),
      Promise.resolve().then(() => {
        console.log('5. third promise');
        return 'c';
      }),
    ]);

    console.log(`6. results: ${results.join(', ')}`);
    // Results are in original order: a, b, c

    console.log('2. after Promise.all call (sync)');

    // Full output:
    // 1. start
    // 2. after Promise.all call (sync)
    // 3. first promise
    // 4. second promise
    // 5. third promise
    // 6. results: a, b, c
  }

  // Promise.race edge case
  async raceEdgeCase(): Promise<void> {
    // What if race contains already-resolved promises?
    const winner = await Promise.race([
      Promise.resolve('first'),
      Promise.resolve('second'),
      Promise.resolve('third'),
    ]);

    console.log(winner); // 'first' - first in array wins ties
  }

  // Promise.allSettled with mixed results
  async allSettledMixed(): Promise<void> {
    const results = await Promise.allSettled([
      Promise.resolve('success'),
      Promise.reject(new Error('fail')),
      Promise.resolve('another success'),
    ]);

    // All promises are processed regardless of rejection
    results.forEach((result, i) => {
      if (result.status === 'fulfilled') {
        console.log(`${i}: fulfilled with ${result.value}`);
      } else {
        console.log(`${i}: rejected with ${result.reason.message}`);
      }
    });
  }
}
software product GIFGIF via GIPHY

Debugging Promise Execution Order

TypeScript
// Tools for debugging Promise execution order
class PromiseDebugger {
  private static idCounter = 0;
  private static timeline: TimelineEntry[] = [];

  // Wrap a promise for tracing
  static trace<T>(
    promise: Promise<T>,
    label: string
  ): Promise<T> {
    const id = ++this.idCounter;

    this.timeline.push({
      id,
      label,
      event: 'created',
      time: performance.now(),
      stack: new Error().stack,
    });

    return promise.then(
      value => {
        this.timeline.push({
          id,
          label,
          event: 'fulfilled',
          time: performance.now(),
          value,
        });
        return value;
      },
      reason => {
        this.timeline.push({
          id,
          label,
          event: 'rejected',
          time: performance.now(),
          reason,
        });
        throw reason;
      }
    );
  }

  // Create a traced Promise.resolve
  static resolveTraced<T>(value: T, label: string): Promise<T> {
    return this.trace(Promise.resolve(value), label);
  }

  // Trace an async function
  static traceAsync<T>(
    fn: () => Promise<T>,
    label: string
  ): Promise<T> {
    this.timeline.push({
      id: ++this.idCounter,
      label,
      event: 'async-start',
      time: performance.now(),
    });

    return fn().then(
      value => {
        this.timeline.push({
          id: this.idCounter,
          label,
          event: 'async-complete',
          time: performance.now(),
          value,
        });
        return value;
      },
      reason => {
        this.timeline.push({
          id: this.idCounter,
          label,
          event: 'async-error',
          time: performance.now(),
          reason,
        });
        throw reason;
      }
    );
  }

  // Print execution timeline
  static printTimeline(): void {
    console.log('\n=== Promise Execution Timeline ===\n');

    const sorted = [...this.timeline].sort((a, b) => a.time - b.time);
    const startTime = sorted[0]?.time ?? 0;

    for (const entry of sorted) {
      const relativeTime = (entry.time - startTime).toFixed(3);
      const valueStr = entry.value !== undefined
        ? ` -> ${JSON.stringify(entry.value)}`
        : entry.reason !== undefined
          ? ` -> ERROR: ${entry.reason}`
          : '';

      console.log(
        `[${relativeTime}ms] #${entry.id} ${entry.label}: ${entry.event}${valueStr}`
      );
    }

    console.log('\n=== End Timeline ===\n');
  }

  static reset(): void {
    this.timeline = [];
    this.idCounter = 0;
  }
}

interface TimelineEntry {
  id: number;
  label: string;
  event: 'created' | 'fulfilled' | 'rejected' | 'async-start' | 'async-complete' | 'async-error';
  time: number;
  value?: unknown;
  reason?: unknown;
  stack?: string;
}

// Usage example
async function debugExample(): Promise<void> {
  PromiseDebugger.reset();

  PromiseDebugger.resolveTraced('A', 'first')
    .then(v => {
      PromiseDebugger.resolveTraced('B', 'nested');
      return v + '-chained';
    });

  PromiseDebugger.resolveTraced('C', 'second');

  await new Promise(r => setTimeout(r, 10));

  PromiseDebugger.printTimeline();
}
software product GIFGIF via GIPHY

Production Incidents

Incident 1: Race Condition from Ordering Assumption

Symptoms: User profile showed stale data 5% of the time after update.

Root Cause: Code assumed Promise resolution order matched call order:

TypeScript
// PROBLEMATIC: Assumed order
async function updateAndFetch(userId: string, updates: UserUpdates) {
  // These execute in parallel
  await api.updateUser(userId, updates);
  const user = await api.getUser(userId);  // May return stale data!

  return user;
}

// The API calls are sequential but the backend might process them
// out of order, or cache might return stale data

Resolution:

TypeScript
// FIXED: Explicit sequencing with proper cache invalidation
async function updateAndFetchSafe(userId: string, updates: UserUpdates) {
  // Wait for update to complete AND propagate
  const updateResult = await api.updateUser(userId, updates);

  // Use version/etag from update to ensure fresh read
  const user = await api.getUser(userId, {
    ifNewerThan: updateResult.version,
    skipCache: true,
  });

  return user;
}

// Alternative: Return updated data from mutation
async function updateAndReturn(userId: string, updates: UserUpdates) {
  // Server returns updated object
  const updatedUser = await api.updateUser(userId, updates, {
    returnUpdated: true,
  });

  return updatedUser;
}

Incident 2: Microtask Queue Starvation

Symptoms: UI froze for 3+ seconds when loading dashboard with complex data transformations.

Root Cause: Deep Promise chains processed all data in microtasks:

The It Crowd Ok GIF by Manny404GIF via GIPHY
TypeScript
// PROBLEMATIC: Recursive Promise chain
function transformDeep(data: NestedData): Promise<TransformedData> {
  return Promise.resolve(data)
    .then(d => transform(d.value))
    .then(transformed => {
      if (data.children) {
        // Creates Promise for EACH child, all in microtask queue
        return Promise.all(
          data.children.map(child => transformDeep(child))
        ).then(children => ({ ...transformed, children }));
      }
      return transformed;
    });
}

// 10,000 nodes = 10,000+ microtasks, no macrotask can run

Resolution:

TypeScript
// FIXED: Chunked processing with macrotask yields
async function transformDeepChunked(
  data: NestedData,
  options = { chunkSize: 100 }
): Promise<TransformedData> {
  let processed = 0;

  async function processNode(node: NestedData): Promise<TransformedData> {
    processed++;

    // Yield every N items
    if (processed % options.chunkSize === 0) {
      await new Promise(r => setTimeout(r, 0));
    }

    const transformed = transform(node.value);

    if (node.children) {
      const children = await Promise.all(
        node.children.map(child => processNode(child))
      );
      return { ...transformed, children };
    }

    return transformed;
  }

  return processNode(data);
}

Incident 3: Unhandled Rejection from Finally

Symptoms: Error tracking showed "Unhandled rejection" but code had .catch().

Root Cause: Promise chain with finally() and error re-throw:

TypeScript
// PROBLEMATIC: Hidden rejection path
async function fetchWithCleanup() {
  let connection: Connection | null = null;

  return connect()
    .then(conn => {
      connection = conn;
      return conn.query('SELECT * FROM users');
    })
    .finally(() => {
      // This throws if connection is null
      connection!.close();
    })
    .catch(err => {
      // This catches query errors, NOT finally errors!
      console.error('Query failed:', err);
    });
}

// If connect() fails, connection is null, finally throws,
// but that throw is NOT caught by the .catch() above it

Resolution:

TypeScript
// FIXED: Proper cleanup error handling
async function fetchWithCleanupSafe() {
  let connection: Connection | null = null;

  return connect()
    .then(conn => {
      connection = conn;
      return conn.query('SELECT * FROM users');
    })
    .finally(() => {
      // Guard against null
      if (connection) {
        return connection.close().catch(closeErr => {
          console.error('Failed to close connection:', closeErr);
        });
      }
    })
    .catch(err => {
      console.error('Operation failed:', err);
      throw err; // Re-throw for caller
    });
}

// Better: async/await with try-finally
async function fetchWithCleanupAsync() {
  const connection = await connect();

  try {
    return await connection.query('SELECT * FROM users');
  } finally {
    try {
      await connection.close();
    } catch (closeErr) {
      console.error('Failed to close connection:', closeErr);
    }
  }
}

Tradeoffs and Engineering Decisions

DecisionChoiceAlternativeWhy This Choice
Promise resolutionAlways asyncSync for already-resolvedPredictable ordering, avoids Zalgo
Thenable assimilationExtra microtaskDirect resolutionSpec compliance, prevents sync recursion
Unhandled rejectionAsync detectionSync throwAllows time for handler attachment
Promise.all failureFail fastWait for allMatches common error handling expectations
Reaction executionFIFO queuePriority basedDeterministic ordering
finally() returnIgnoredPropagatedCleanup shouldn't change resolution
Combinator iterationEager (sync)LazyPredictable Promise creation timing
Error propagationSkip .then()Call with undefinedClearer error flow
Working Work From Home GIF by TecocraftGIF via GIPHY

Key Takeaways

  1. .then() handlers are ALWAYS async - even on already-resolved Promises, handlers queue as microtasks
  2. Returning a Promise from .then() adds an extra microtask - the PromiseResolveThenableJob delays resolution
  3. Multiple await statements create sequential microtasks - they interleave with other Promise chains
  4. Promise.all resolves AFTER all input Promises - the completion callback is a separate microtask
  5. Microtasks are FIFO - handlers execute in the order they were queued within the same checkpoint
  6. finally() creates additional microtask ticks - return values are ignored but async operations wait
  7. Unhandled rejections are detected asynchronously - allowing time for .catch() to be attached
  8. Error propagation skips .then() onFulfilled - chain jumps to nearest rejection handler
  9. Promise.resolve(promise) returns the same promise - no wrapping for native Promises
  10. Executor runs synchronously - new Promise(executor) calls executor before returning
Listen Episode 11 GIF by The BachelorGIF via GIPHY

What did you think?

© 2026 Vidhya Sagar Thakur. All rights reserved.