ECMAScript Await Continuations and Promise Jobs

Aug 17, 2026·22 min read

await looks like a pause in the middle of a function, but the language cannot leave that function sitting on the execution stack while other JavaScript runs. It records how to resume, returns control to the caller, and schedules the resumption when the awaited promise is ready.

An ECMAScript await continuation resumes a captured async execution through a PromiseReactionJob, which a browser host places in its microtask queue after the awaited promise is fulfilled or rejected.

The Short Answer: Await Resumes Through a Promise Job

An await continuation is the work that continues an async function after an await expression. It is not a callback placed in a queue to watch a promise, and it does not occupy the browser’s microtask queue while a promise remains pending.

Say an async function reaches this line:

const profile = await loadProfile();
console.log(profile.name);

The async evaluation pauses at await. Control returns to the code that called the async function. When the promise from loadProfile() is fulfilled, the suspended evaluation resumes, profile receives the fulfillment value, and the next statement can run.

That resumption is always asynchronous. The rule still applies when the value is already available:

async function showOrder() {
  console.log('before await');
  await 42;
  console.log('after await');
}

console.log('script start');
showOrder();
console.log('script end');
script start
before await
script end
after await

42 is not pending on anything, but after await still runs later. The containing async evaluation pauses; the rest of the script does not.

Before awaitSuspendedLatermicrotaskCallerAsyncat awaitSavedcontextLocals + placeCaller runsCallerAsyncresumes
Await saves the async function’s place while its caller continues.

The path crosses two specifications. ECMAScript defines the promise reactions and Jobs that resume the async execution. HTML defines how a browser schedules promise-related Jobs as microtasks. Calling both layers “the microtask queue” hides the point where the language hands work to its host.

That distinction becomes useful as soon as await, .then(), queueMicrotask(), and timers meet. Async/await gives you the surface syntax. Here we follow the machinery underneath it.

Jobs, Reactions, and Microtasks Are Different Layers

Start with the names. They describe related things, but they do not describe the same thing.

  • An ECMAScript Job is a unit of scheduled language work. A PromiseReactionJob is the kind that runs a promise reaction.
  • A PromiseReaction record stores a fulfillment or rejection handler together with information needed to process its result. A pending promise retains these records until it settles.
  • A host is the environment embedding ECMAScript. A browser is one host; another runtime can supply different scheduling rules around the language.
  • An HTML microtask is work in a browser event loop’s microtask queue. HTML maps promise-related ECMAScript Jobs into this queue.
  • An HTML task runs work such as a timer callback. Tasks and microtasks occupy different queues.
  • A microtask checkpoint repeatedly takes the oldest microtask and runs it until the queue is empty.

The bridge is HostEnqueuePromiseJob. ECMAScript calls this host hook when a promise Job is ready to be scheduled. In a browser, HTML’s HostEnqueuePromiseJob queues a microtask that runs the Job.

A stored reaction is not an enqueued microtask. If a promise is pending, its reactions wait inside the promise. Settlement turns those records into Jobs, and the host then schedules those Jobs.

Reaction becomes a microtaskECMAScriptBrowser hostPendingpromiseStoredreactionsPromiseJobsettlesHosthookMicrotaskqueueRuns Job
Settlement converts a stored reaction into a Job that the browser can queue.

Promise construction shows the other half of the timing rule. The executor passed to new Promise() runs synchronously, but a reaction attached with .then() never runs inline:

console.log('before constructor');

const booking = new Promise((resolve) => {
  console.log('inside executor');
  resolve('confirmed');
});

booking.then((status) => {
  console.log(status);
});

console.log('after then');
before constructor
inside executor
after then
confirmed

The executor calls resolve before booking.then(...) is reached, so the promise is already fulfilled when the handler is attached. Attaching the handler creates ready reaction work and asks the host to enqueue it. The handler still waits for a microtask checkpoint.

Promises have pending, fulfilled, and rejected states. Keep resolved separate from fulfilled: a promise can be resolved to follow another promise and remain pending until that other promise settles. Promise basics develops those states from the API side.

A browser checkpoint drains until no microtasks remain. If one microtask appends another, the new one runs during the same checkpoint, after microtasks already waiting ahead of it. That is the scheduling model described in Microtasks and connected to tasks in Event loop: microtasks and macrotasks.

What the ECMAScript Await Operation Actually Does

The Await abstract operation spells out what happens after an async function evaluates the operand on the right of await.

Translate its work into six moves.

  1. PromiseResolve(%Promise%, value) produces a promise for the awaited value. If value is an appropriate native promise, it can be used directly. A plain value produces an already-fulfilled promise.

  2. Await records the running async execution context. This context represents the suspended evaluation that must later continue after the await expression.

  3. Await creates fulfillment and rejection closures. Both closures capture that async execution context. The fulfillment closure knows how to resume it with a normal completion; the rejection closure knows how to resume it with a throw completion.

  4. Await passes the promise and those closures to PerformPromiseThen. This is an internal specification operation. PerformPromiseThen attaches to the normalized intrinsic promise without calling that promise’s public .then(), although PromiseResolve may read the original operand’s public then property and a later NewPromiseResolveThenableJob may invoke the captured method.

  5. Await removes the async execution context from the execution-context stack and resumes the caller. Only the containing async evaluation pauses.

  6. A fulfillment or rejection closure eventually restores the captured context and resumes it. The value of the await expression is either the fulfillment value or a thrown rejection reason.

Here is the boundary you can observe:

async function prepareInvoice() {
  console.log('invoice: start');

  const status = await Promise.resolve('paid');

  console.log(`invoice: ${status}`);
  return 1999;
}

console.log('caller: before');

const result = prepareInvoice();

console.log('caller: received promise');

result.then((cents) => {
  console.log(`caller: ${cents}`);
});

console.log('caller: after');
caller: before
invoice: start
caller: received promise
caller: after
invoice: paid
caller: 1999

prepareInvoice() runs synchronously until it reaches await. It then returns a promise to its caller, so caller: received promise and caller: after print before the continuation.

When the await continuation runs, it prints invoice: paid and returns 1999. That return fulfills result, making the result.then(...) reaction ready. The final handler therefore runs as later reaction work, not as part of the await continuation already in progress.

Promise chains follow the same shape. One reaction runs, its result settles another promise, and that settlement makes the next reaction eligible. A chain is successive work, not one microtask containing the entire chain. Promises chaining shows how returned values and promises feed those later reactions.

A promise chain can be interleavedBeforeChain Job 1Other microtaskChain Job 1is runningNext promisesettlesAfterOther microtaskChain Job 2
Each chain link earns a new queue position, so other microtasks can appear between links.

The captured execution context is the missing piece in the phrase “await resumes later.” Local bindings and the current point in the async evaluation still matter after suspension because the closures created by Await retain the context needed to continue.

One saved context, two outcomesAwaited promisesettlesFulfillmentclosureRejectionclosureSaved async contextlocals + next instructionnormal valuethrow reason
Both await outcomes restore the same saved context, with either a value or a throw.

The payoff is code that reads from top to bottom. The price is that every await introduces an asynchronous boundary, even when the value is already fulfilled. Async, Modules & Modern JavaScript follows that boundary through promise composition, modules, and the event loop in the offline course.

Pending Versus Fulfilled: When Is the Job Enqueued?

PerformPromiseThen takes different branches depending on the promise’s state. The difference is registration versus scheduling.

Awaited promise stateWhat PerformPromiseThen doesIs a promise Job enqueued now?
PendingAppends fulfillment and rejection PromiseReaction records to the promise’s internal listsNo
Already fulfilledCreates a fulfillment PromiseReactionJob and calls HostEnqueuePromiseJobYes
Already rejectedCreates a rejection PromiseReactionJob and calls HostEnqueuePromiseJobYes

The first row is the one that queue explanations often lose.

State decides: store or schedulePerformPromiseThenpendingfulfilled orrejectedPromiseReaction recordsQueue unchangedReactionJobMicrotask queue
PerformPromiseThen stores on the pending branch and schedules on the settled branch.

Say await responsePromise receives a promise that is still waiting for a response. PerformPromiseThen creates the two reaction records used by Await and stores them on that pending promise. There is no continuation microtask circling the queue and checking its state.

Later, the promise fulfills. TriggerPromiseReactions takes each stored fulfillment reaction, creates a PromiseReactionJob for it, and asks the host to enqueue that Job. The browser then adds the corresponding microtask.

Now take await Promise.resolve('ready'). The promise is already fulfilled when PerformPromiseThen sees it. The operation creates the fulfillment Job immediately and calls HostEnqueuePromiseJob. “Immediately” describes enqueueing, not running. The current synchronous code continues until the host reaches a microtask checkpoint.

A plain value follows the fulfilled side after PromiseResolve wraps it:

async function readValue() {
  console.log('reading');
  const value = await 'ready';
  console.log(value);
}

readValue();
console.log('outside');
reading
outside
ready

The value needs no future event, but the continuation still enters scheduling machinery. That is why removing an apparently redundant await can change ordering.

A rejected promise takes the same timing branch as a fulfilled one when it is already settled. The difference appears when its rejection closure resumes the async execution with a throw completion.

Trace the Queue One Statement at a Time

One browser example makes all four scheduling forms meet. Run this as a classic script:

const ready = Promise.resolve('ready');

async function report() {
  console.log('async start');
  await ready;
  console.log('await continuation');

  Promise.resolve().then(() => {
    console.log('then inside continuation');
  });
}

console.log('script start');

report();

Promise.resolve().then(() => {
  console.log('outer then');
});

queueMicrotask(() => {
  console.log('queued microtask');

  Promise.resolve().then(() => {
    console.log('then from queued microtask');
  });
});

setTimeout(() => {
  console.log('timer task');
}, 0);

console.log('script end');
script start
async start
script end
await continuation
outer then
queued microtask
then inside continuation
then from queued microtask
timer task

Now keep a ledger. The queue shown here is the browser’s microtask queue, not a universal ECMAScript queue.

Scheduling eventImmediate outputBrowser microtask queue after the event
console.log('script start')script startempty
report() startsasync startempty before await
await ready sees a fulfilled promisenoneawait fulfillment Job
outer .then(...) sees a fulfilled promisenoneawait fulfillment Job, outer reaction Job
queueMicrotask(...) runsnoneawait fulfillment Job, outer reaction Job, queued callback
setTimeout(...) registers a later tasknoneunchanged
final synchronous log runsscript endunchanged

The script task is now finished, so the browser can perform a microtask checkpoint. HTML repeatedly dequeues the oldest microtask while the queue remains nonempty.

The await fulfillment Job runs first. It restores report’s captured async execution context, so await continuation prints. The .then(...) inside that continuation sees an already-fulfilled promise and appends its reaction Job to the end of the queue:

Checkpoint stepOutputBrowser microtask queue afterwards
Await continuationawait continuationouter reaction Job, queued callback, inner reaction Job
Outer reactionouter thenqueued callback, inner reaction Job
queueMicrotask callbackqueued microtaskinner reaction Job, reaction Job added by queued callback
Inner reactionthen inside continuationreaction Job added by queued callback
Reaction created inside queued callbackthen from queued microtaskempty

The queue reaches empty only after running the two Jobs appended during the checkpoint. Then the later timer task can run and print timer task.

The checkpoint drains a moving queueoldesttailJob ACallbackNew JobRun oldestmicrotaskQueueempty?Later timertaskyesno: looprunning code may append
A microtask checkpoint keeps looping until running code stops adding more work.

Nothing in this order depends on treating await as a special queue with priority. The await continuation, promise handlers, and queueMicrotask callback all participate in browser microtask ordering. Ready work is appended in enqueue order.

Change ready to a pending promise and the first ledger changes. report() stores its await reactions but adds no microtask at that point. Whichever event later fulfills ready causes its reaction Job to be enqueued at that later position.

Timers belong to the host’s task machinery, which is why the timer waits until the current task and its microtask checkpoint finish. Scheduling with setTimeout and setInterval covers what delay values do and do not promise.

Thenables, Rejections, and Extra Promise Jobs

A thenable is an object with a callable then property. It can participate in promise resolution without being a native Promise.

Thenables add another possible piece of scheduled work. PromiseResolve obtains a promise for the value, and promise resolution can create a NewPromiseResolveThenableJob. That Job invokes the captured then method after the surrounding synchronous code has completed.

Here is the observable order:

const reservation = {
  then(resolve) {
    console.log('thenable method');
    resolve('confirmed');
  },
};

async function checkReservation() {
  console.log('check start');
  const status = await reservation;
  console.log(status);
}

checkReservation();
console.log('script end');
check start
script end
thenable method
confirmed

The thenable method does not run while the await expression is first evaluated. Its Job runs later, resolves the promise being awaited, and that settlement makes the stored await reaction ready. The continuation follows as further promise work.

A thenable adds an extra JobCurrent scriptLater microtasksawait capturesthen methodScript endsJob 1: call thenable.thenwith resolve and rejectAwaited promise settlesJob 2: resume after awaitwith the settled result
Thenable assimilation and await resumption are separate scheduled pieces of work.

Rejection changes the completion delivered to the captured context:

async function saveDraft() {
  try {
    await Promise.reject(new Error('storage unavailable'));
  } catch (error) {
    console.log(error.message);
  }
}

saveDraft();
console.log('save requested');
save requested
storage unavailable

Await’s rejection closure resumes the captured execution context with a throw completion. The rejection therefore behaves like a throw at the await expression, and the surrounding try...catch receives it. Error handling with promises follows the same rejection through longer chains.

Rules for Predicting Await Order Correctly

Start with synchronous execution and write down each output until the current call stack finishes. Promise executors belong in this pass.

When code reaches await, apply PromiseResolve, then inspect the resulting promise’s state. A pending promise stores reactions. An already-fulfilled or rejected promise causes the appropriate PromiseReactionJob to be created and handed to the host.

Keep stored reactions out of the microtask ledger. Add them only when settlement turns them into Jobs.

For browser code, append ready promise Jobs and queueMicrotask callbacks to the microtask queue in enqueue order. At the checkpoint, remove the oldest entry, run it, record any new microtasks it appends, and continue until the queue is empty.

Only then move to a later task such as a timer callback. Treat runtime-specific queues separately, because ECMAScript defines the language Jobs and host hooks, while the host defines how those requests fit its event loop.

That procedure handles plain values, pending promises, fulfilled promises, rejection, thenable assimilation, and promise chains without guessing.

Frequently asked questions

Is an await continuation a microtask?
An await continuation is the work that resumes an async execution after await settles. ECMAScript arranges that work through a PromiseReactionJob, and HTML places that job in the browser's microtask queue.
Does await always resume asynchronously?
Yes. Await suspends the containing async evaluation and resumes it later, even when its operand is a plain value or an already-fulfilled promise. Code after await never runs inline with code before it.
Does awaiting a pending promise immediately add a microtask?
No. PerformPromiseThen stores fulfillment and rejection PromiseReaction records on the pending promise. A job is created and enqueued only when that promise settles.
Does await call the promise's public then method?
Not always. For an intrinsic Promise produced by the same constructor, PerformPromiseThen attaches internally without calling its public .then(). For a thenable, Promise subclass instance, or cross-realm Promise, PromiseResolve can read its public then property and a later NewPromiseResolveThenableJob can invoke the captured method.
Do promise chains run in one microtask?
No. Each settled link makes the next reaction eligible to run as separate job work. Other microtasks can therefore run between links when their enqueue order places them there.