Learning path · Level 7 · Tutorial 1

Sequence Asynchronous Work with async/await

Write asynchronous functions that wait for dependent results, handle failures, and perform cleanup. Use small delayed operations on your own computer to observe execution order before introducing network requests.

  • Level: Intermediate
  • Duration: 25 min
  • 9 steps
Start tutorial Back to the path
Sequence Asynchronous Work with async/await

What you will go through

  1. Recognize a Promise and Its Outcome Recognize promise states and distinguish a promise from the result or failure it eventually represents. 2 min
  2. Create a Controlled Delayed Operation Build a local promise-returning helper that settles after a controlled delay, without consuming its result or invoking its failure branch yet. 3 min
  3. Understand What async Returns Recognize that every async function call returns a promise, then predict whether that promise fulfills with a returned value or rejects from an uncaught throw. 3 min
  4. Trace Execution Across await Predict, run, and verify how an async workflow and its caller proceed around one awaited delayed result. 3 min
  5. Sequence Work That Depends on a Result Use successive awaited assignments to wait for a profile and then create a greeting from its result. 4 min
  6. Catch Failures at the Awaited Operation Handle rejected awaited operations locally with one try/catch and observe which dependent work is skipped. 3 min
  7. Run Cleanup on Both Paths Restore temporary workflow activity with finally after successful or failed awaited work. 2 min
  8. Handle the Promise at the Call Site Ensure that a rejection escaping an async workflow is handled by its caller. 2 min
  9. Verify the Complete Async Workflow Complete and locally verify a dependent asynchronous workflow with reliable failure handling and cleanup. 3 min

What you will learn

  • Distinguish a pending promise from its eventual fulfilled value or rejection.
  • Predict the order of observable output around an awaited operation.
  • Sequence two asynchronous operations when the second requires the first result.
  • Handle a rejection and verify that cleanup runs on both success and failure.

Before you start

  • Write Functions with Parameters and Lexical Scope
  • Handle Failures with JavaScript Exceptions
  • Run a Local Development Server with Vite

Step 1 of 9

Recognize a Promise and Its Outcome

Recognize promise states and distinguish a promise from the result or failure it eventually represents.

A promise has an outcome later

An eventual outcome

A promise is an object that represents an outcome that will be available later. It is not the outcome itself.

Think of a delayed profile operation. While it is unfinished, you have a promise—not the profile. If it succeeds later, the promise provides a fulfillment value. If it fails later, it provides a rejection reason.

A promise begins in the pending state.

Promise state flow

A pending promise can move to either a fulfilled outcome or a rejected outcome.

The amber node represents pending. The green and red end nodes represent the two possible settled outcomes.

State and outcome are different ideas

Read the states precisely

A promise has three states:

  • pending: the operation is unfinished. This is not a failure.
  • fulfilled: it finished successfully with a fulfillment value.
  • rejected: it finished unsuccessfully with a rejection reason, often an Error.

Fulfilled and rejected are both settled. Once a promise is settled, it cannot switch to the other outcome or become pending again.

Example

Promise versus eventual value

Imagine an operation intended to provide a profile.

Immediately after it starts, profilePromise refers to the promise: an object representing the future result. It is not yet the profile object, so it does not stand for properties such as name.

If that promise fulfills, its fulfillment value might be { id: 7, name: "Maya" }. That profile object is the result the promise eventually provided—not the promise itself.

If it rejects instead, there is no fulfillment value from that operation; there is a rejection reason to handle.

Tip

Useful distinction

Use the state to describe completion, then use the outcome to describe what completion produced.

Check your understanding

Classify the promise scenario

Match each scenario to its best description.

Tap an item and then its matching pair.

Step 2 of 9

Create a Controlled Delayed Operation

Build a local promise-returning helper that settles after a controlled delay, without consuming its result or invoking its failure branch yet.

Separate immediate setup from later settlement

One helper, two possible outcomes

delayResult will create a promise whose outcome is controlled by a timer. Constructing the promise runs its executor immediately. The callback passed to setTimeout runs later, after at least the requested delay.

When that later callback runs, it chooses one outcome:

  • The normal branch fulfills with the supplied value.
  • The failure branch rejects with an Error whose message is "Practice failure".

The operation is still pending while the timer is waiting. A delay of 600 ms is a scheduling threshold, not a guarantee of an exact completion time.

From construction to settlement

A promise starts a timer immediately, then the timer later routes to either a value outcome or an error outcome.

The executor starts during promise construction. The timer callback makes the later fulfillment-or-rejection decision.

Build and inspect the practice helper

Run a successful local operation

Replace the contents of your local Vite project's two practice files. Open the browser console, then reload the page.

This first run calls the helper without its optional failure argument, so it uses the successful branch only. The logged promise is initially pending; browser consoles may update its expandable preview after it settles.

index.html

Replace index.html.

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Async Practice</title>
  </head>
  <body>
    <script type="module" src="./main.js"></script>
  </body>
</html>

main.js

Replace main.js.

javascript
function delayResult(value, shouldFail = false) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldFail) {
        reject(new Error("Practice failure"));
        return;
      }

      resolve(value);
    }, 600);
  });
}

const profilePromise = delayResult({ id: 7, name: "Maya" });
console.log(profilePromise);
Expected output
Promise {<pending>}

The console initially represents the promise as pending. Its exact visual formatting and when its expandable preview updates vary by browser; after the timer completes, inspect it to see the fulfilled profile value.

Tip

Keep this run controlled

Do not call delayResult with true yet. That would intentionally create a rejection before this tutorial has introduced a rejection handler. For now, examine how the helper is built and observe the successful promise settle.

Practice: construct, delay, and settle

Complete the settlement paths

Complete the controlled delayed helper. Use the branch structure to identify which outcome supplies an error and which supplies the provided value.

javascript
function delayResult(value, shouldFail = false) {
  return new ((resolve, reject) => {
    (() => {
      if (shouldFail) {
        (new ("Practice failure"));
        return;
      }

      ();
    }, 600);
  });
}

Step 3 of 9

Understand What async Returns

Recognize that every async function call returns a promise, then predict whether that promise fulfills with a returned value or rejects from an uncaught throw.

Recognize the return wrapper

An async call returns a promise

Adding async changes what a caller receives. A regular function call provides its returned value directly. An async function call provides a promise immediately, even when the function body contains no await.

A normal return inside an async function becomes the fulfillment value of that returned promise. An uncaught throw becomes its rejection reason. The promise is therefore different from the value or error it eventually represents.

Direct return versus promised outcome

A comparison diagram showing a regular function directly returning a greeting, while an async function returns a promise that later leads to either a greeting or an error.

A regular call gives the caller a direct result. An async call gives the caller a promise whose outcome is determined by return or an uncaught throw.

Direct return versus async return

Compare what the caller receives with what happens inside each function body.

Within an async function, return and uncaught throw determine the settlement of its returned promise.
Function kindCaller receivesNormal returnUncaught throw
Regular functionDirect valueCaller receives that valueCaller receives a synchronous exception
Async functionPromisePromise fulfills with that valuePromise rejects with that error

Follow the two static paths

No await is required

This example deliberately contains no await. It is still an async function, so sequencePromise is a promise rather than the greeting string.

With the default selector, execution reaches return greeting;, so the returned promise fulfills with the greeting. With the "profile" selector, the uncaught throw makes the returned promise reject with an Error instead. Read that failure path as a prediction only: do not execute runSequence("profile") at this stage.

Return and throw determine the promise outcome

Study this static example. It intentionally has no await. You may use the default "none" call as written, but do not change it to "profile" or execute a rejection case yet; rejection handling is introduced later.

javascript
async function runSequence(failAt = "none") {
  if (failAt === "profile") {
    throw new Error("Practice failure");
  }

  const user = { id: 7, name: "Maya" };
  const greeting = `Hello, ${user.name}!`;
  return greeting;
}

const sequencePromise = runSequence("none");

Tip

Read return in async context

return greeting; does not make the caller receive greeting directly. Because runSequence is async, it settles sequencePromise successfully with that string. The uncaught throw takes the other path: it makes the returned promise reject.

Classify the returned promise

Classify the returned promise

Match each code moment from the static example to the outcome it produces. Classify both paths without running the failure selector.

Tap an item and then its matching pair.

Step 4 of 9

Trace Execution Across await

Predict, run, and verify how an async workflow and its caller proceed around one awaited delayed result.

Separate the workflow from its caller

What pauses at await?

One function pauses; the caller does not

Inside an async function, await obtains the fulfillment value of a promise. When execution reaches its first await, the function's continuation pauses until that promise settles.

Calling delayResult(...) starts the delayed operation. await does not start it again—it waits for the outcome that the call already represented.

The caller receives a promise from runSequence() and can continue running ordinary statements. This waiting does not pause the entire browser, and it is not a way to move CPU-heavy work off the main thread.

Workflow and caller timeline

A two-lane timeline shows the workflow pausing after starting a promise while the caller continues to a console action; later the workflow receives a profile result.

The workflow pauses at await; the caller continues and logs before the delayed profile is ready.

Predict before you run

Predict the console order

Before running the practice code, arrange these observable console labels from first to last.

  1. Profile ready
  2. Caller continues
  3. Sequence started

Verify the two lanes locally

Run one awaited operation

Keep your existing Vite index.html entry with <script type="module" src="./main.js"></script>. Replace the contents of main.js with the code below, reload the local page, and inspect the browser console.

Run only runSequence("none") here. Do not use elapsed time as the check: the important evidence is the relative order of the three logs.

Observe execution around await

main.js

javascript
function delayResult(value, shouldFail = false) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldFail) {
        reject(new Error("Practice failure"));
        return;
      }

      resolve(value);
    }, 600);
  });
}

async function runSequence(failAt = "none") {
  console.log("Sequence started");

  const user = await delayResult(
    { id: 7, name: "Maya" },
    failAt === "profile"
  );

  console.log("Profile ready", user);
}

runSequence("none");
console.log("Caller continues");
Expected output
Sequence started
Caller continues
Profile ready { id: 7, name: "Maya" }

Object formatting can differ slightly between browser developer tools. Check the log order, not an exact elapsed time.

Apply the awaited assignment

Complete the awaited assignment

Complete the assignment so user receives the delayed operation's fulfillment value.

javascript
async function runSequence(failAt = "none") {
  console.log("Sequence started");

  const user =  delayResult(
    { id: 7, name: "Maya" },
    failAt === "profile"
  );

  console.log("Profile ready", user);
}

Confirm the observed order

Report your local verification

After running the successful case locally, report your observation in one to three sentences. State whether the console matched your prediction and list the three labels in the order you observed. Do not use timestamps or precise elapsed times.

Write at least 45 characters (0/45).

Step 5 of 9

Sequence Work That Depends on a Result

Use successive awaited assignments to wait for a profile and then create a greeting from its result.

Follow the profile data

Let the data determine the order

The greeting operation has a data dependency: it needs the profile’s name. That means the profile operation must fulfill first.

Store that fulfilled profile in user. Only then can the workflow use user.name to create the greeting input. Two successive awaited assignments express this order clearly—without nested callbacks.

Profile result feeds the greeting

A flow diagram where a fulfilled profile object provides its name to start a greeting operation, which then produces a greeting message.

The greeting operation begins only after the profile result provides user.name.

Run the success path

main.js: dependent profile and greeting

Keep your existing index.html module script entry. Replace the contents of main.js with this success-only two-operation workflow, then reload the Vite page and inspect the browser console.

javascript
function delayResult(value, shouldFail = false) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldFail) {
        reject(new Error("Practice failure"));
        return;
      }

      resolve(value);
    }, 600);
  });
}

async function runSequence() {
  console.log("Sequence started");

  const user = await delayResult({ id: 7, name: "Maya" });
  console.log("Profile ready", user);

  const greeting = await delayResult(`Hello, ${user.name}!`);
  console.log("Greeting ready", greeting);
}

runSequence();
console.log("Caller continues");
Expected output
Sequence started
Caller continues
Profile ready { id: 7, name: "Maya" }
Greeting ready Hello, Maya!

The browser may display the logged object with slightly different spacing or an expandable preview. The order of the four messages is the important result.

Use the fulfilled value, not its promise

A promise is not the profile

delayResult(...) immediately gives back a promise. That promise represents the future profile; it is not the profile object itself.

Reading a property from the promise does not wait for its fulfillment. Await the promise and use the resulting user value instead.

Promise versus fulfilled value

This contrast is for study. The first property read is on the promise object; the second is on the fulfilled profile object.

javascript
async function showName() {
  const userPromise = delayResult({ id: 7, name: "Maya" });
  console.log(userPromise.name); // undefined

  const user = await delayResult({ id: 7, name: "Maya" });
  console.log(user.name); // Maya
}

Tip

The first await creates a usable user binding. The second delayed operation can therefore receive a string built from that profile’s name.

Complete and verify the sequence

Build the dependency

Complete the two dependent assignments.

javascript
async function runSequence() {
  console.log("Sequence started");

  const user =  delayResult({ id: 7, name: "Maya" });
  console.log("Profile ready", user);

  const greeting = await delayResult(`Hello, !`);
  console.log("Greeting ready", greeting);
}

Verify the dependent sequence

After completing the code and running it locally, explain why the profile must arrive first. Then report the order of the two readiness logs and the final greeting string you observed.

Write at least 60 characters (0/60).

Step 6 of 9

Catch Failures at the Awaited Operation

Handle rejected awaited operations locally with one try/catch and observe which dependent work is skipped.

Leave the protected sequence when await rejects

A rejection changes the path

A rejected promise does not continue as a normal value. At the await expression, its rejection reason is thrown. A surrounding try/catch receives that reason, and JavaScript skips every later statement remaining in the try block.

Put both dependent awaits in one try block. If the profile operation rejects, the greeting operation never starts. If the greeting operation rejects, the profile has already succeeded, but the greeting-success log is skipped.

In this step, catch handles the failure locally and completes normally.

Protected dependent operations

A profile operation leads to a greeting operation inside one protected path. A profile failure bypasses the greeting, while failures from either operation converge at one catch destination.

One catch handles either rejected await. The point of failure determines which later work is skipped.

Protect both awaits in main.js

Run one controlled case at a time

Keep your existing index.html entry file. Replace main.js with this complete version, reload the Vite page, and inspect the browser console.

Start with "none". To test a failure, change only the value assigned to failAt to "profile" or "greeting", then reload. Run one selector per reload. The catch intentionally does not rethrow, so each selected failure is handled inside this local workflow.

main.js

Replace main.js, then run one selector per reload. The output below is for the selected "none" case.

javascript
function delayResult(value, shouldFail = false) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldFail) {
        reject(new Error("Practice failure"));
        return;
      }

      resolve(value);
    }, 600);
  });
}

async function runSequence(failAt = "none") {
  console.log("Sequence started");

  try {
    const user = await delayResult(
      { id: 7, name: "Maya" },
      failAt === "profile"
    );
    console.log("Profile ready", user);

    const greeting = await delayResult(
      `Hello, ${user.name}!`,
      failAt === "greeting"
    );
    console.log("Greeting ready", greeting);
  } catch (error) {
    console.error("Sequence interrupted", error.message);
  }
}

const failAt = "none";
runSequence(failAt);
Expected output
Sequence started
Profile ready { id: 7, name: "Maya" }
Greeting ready Hello, Maya!

The displayed output is produced by the code as written with `failAt = "none"`. Expected trace when you change only `failAt` to `"profile"` (editorial heading, not console output): Sequence started Sequence interrupted Practice failure Expected trace when you change only `failAt` to `"greeting"` (editorial heading, not console output): Sequence started Profile ready { id: 7, name: "Maya" } Sequence interrupted Practice failure Browser formatting of the profile object can vary slightly. Compare the labels and their order.

Tip

Observe one workflow at a time

The delay is at least 600 ms, but do not use elapsed time as your check. Compare each selector with its trace: a profile failure has no readiness log, while a greeting failure has the profile readiness log but no greeting readiness log.

Practice the protected structure

Complete the local failure handler

Complete the control-flow keywords for the protected asynchronous sequence.

javascript
async function runSequence(failAt = "none") {
  console.log("Sequence started");

   {
    const user =  delayResult(
      { id: 7, name: "Maya" },
      failAt === "profile"
    );
    console.log("Profile ready", user);

    const greeting = await delayResult(
      `Hello, ${user.name}!`,
      failAt === "greeting"
    );
    console.log("Greeting ready", greeting);
  }  (error) {
    console.error("Sequence interrupted", error.message);
  }
}

Verify every failure selector

Predict, run, and compare

Use the completed structure in main.js. Run "none", "profile", and "greeting" separately, reloading after each selector change.

Before each reload, predict the last successful delayed operation. Then compare the console trace with the expected traces on the previous screen. In your response, state that both awaits are protected together, identify the last successful delayed operation for every selector (or state that none completed), and say whether each observed trace matched your prediction.

Write at least 90 characters (0/90).

Step 7 of 9

Run Cleanup on Both Paths

Restore temporary workflow activity with finally after successful or failed awaited work.

Converge on finally

One cleanup point for two paths

finally is the shared exit point for a protected asynchronous sequence. Set isRunning = true; before the try block, then restore it in finally.

After a successful sequence, control reaches finally. After a rejected awaited operation is handled by catch, control also reaches finally. This prevents duplicated cleanup in separate success and error branches.

Both paths converge on cleanup

A success path and a handled-failure path join at one finally cleanup block that restores an activity flag to false.

Put work that must happen on either path in finally.

Tip

Protect the intended outcome

Keep finally focused on cleanup. Do not put a return statement or a new throw there: either could replace the result or failure that the protected sequence was otherwise producing.

Run and compare the three cases

Make cleanup observable

Keep your existing index.html module entry. Replace main.js with this complete local practice. Start with the selected success case, then change only the final selector to "profile" or "greeting" and reload once per case. The catch handles failures locally in this step; cleanup still runs afterward.

javascript
function delayResult(value, shouldFail = false) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldFail) {
        reject(new Error("Practice failure"));
        return;
      }

      resolve(value);
    }, 600);
  });
}

let isRunning = false;

async function runSequence(failAt = "none") {
  isRunning = true;
  console.log("Sequence started", isRunning);

  try {
    const user = await delayResult(
      { id: 7, name: "Maya" },
      failAt === "profile"
    );
    console.log("Profile ready", user);

    const greeting = await delayResult(
      `Hello, ${user.name}!`,
      failAt === "greeting"
    );
    console.log("Greeting ready", greeting);
  } catch (error) {
    console.error("Sequence interrupted", error.message);
  } finally {
    isRunning = false;
    console.log("Cleanup complete", isRunning);
  }
}

runSequence("none");
Expected output
Sequence started true
Profile ready { id: 7, name: "Maya" }
Greeting ready Hello, Maya!
Cleanup complete false

For `runSequence("profile")`, the complete trace is: Sequence started true Sequence interrupted Practice failure Cleanup complete false For `runSequence("greeting")`, the complete trace is: Sequence started true Profile ready { id: 7, name: "Maya" } Sequence interrupted Practice failure Cleanup complete false Browser consoles may render the profile object differently. Compare the labels, their order, and the relevant values rather than exact object formatting.

Verify each path

Run one selector per reload. Every trace ends with cleanup, but each failure stops a different amount of dependent work.

Complete expected console traces after the delayed operations settle.
SelectorComplete expected trace
"none"Sequence started true Profile ready { id: 7, name: "Maya" } Greeting ready Hello, Maya! Cleanup complete false
"profile"Sequence started true Sequence interrupted Practice failure Cleanup complete false
"greeting"Sequence started true Profile ready { id: 7, name: "Maya" } Sequence interrupted Practice failure Cleanup complete false

Place the cleanup once

Complete reliable cleanup

Complete the shared cleanup for a successful sequence and for either locally handled failure.

javascript
async function runSequence(failAt = "none") {
  isRunning = true;

  try {
    const user = await delayResult(
      { id: 7, name: "Maya" },
      failAt === "profile"
    );
    console.log("Profile ready", user);

    const greeting = await delayResult(
      `Hello, ${user.name}!`,
      failAt === "greeting"
    );
    console.log("Greeting ready", greeting);
  } catch (error) {
    console.error("Sequence interrupted", error.message);
  }  {
    isRunning = ;
    console.log("Cleanup complete", isRunning);
  }
}

Confirm the three outcomes

Verify cleanup on every outcome

After completing the cleanup and running "none", "profile", and "greeting" on separate reloads, report your verification in two to five sentences. State which block contains the shared cleanup, list the isRunning value logged with Cleanup complete for all three selectors, and identify which failure trace included Profile ready.

Write at least 80 characters (0/80).

Step 8 of 9

Handle the Promise at the Call Site

Ensure that a rejection escaping an async workflow is handled by its caller.

Handle the returned promise at its boundary

A promise can outlive synchronous try/catch

Calling an async workflow gives the caller a promise immediately. A synchronous try/catch around an unawaited call ends before that promise may reject, so it cannot catch a later rejection.

At a non-async entry point, attach .catch(reportFailure) as the terminal boundary handler. This tutorial uses .catch() only for that final handling step—not as a general chaining style.

Where an escaping rejection goes

A two-lane diagram showing a workflow's rejection passing through finally cleanup and then reaching the caller's terminal catch handler.

A rejection that is rethrown by the workflow remains on its returned promise until the caller handles it.

Two valid caller boundaries

Choose the handling form that matches the caller's context.

Both forms observe the promise returned by the workflow.
Caller contextHandling formResult
Async callerawait inside try/catchCatch receives rejection
Non-async entry point.catch(reportFailure)Handler receives rejection
Synchronous try around unawaited calltry { runSequence(); }Later rejection escapes it

Rethrow, clean up, then report

Run one handled failure case

Replace your entire main.js with this version. It deliberately runs the greeting-failure case. The workflow records the interruption, rethrows the same error, and still performs cleanup. The caller's terminal handler reports the escaping rejection.

Reload once and inspect the browser console. The caller continues after the workflow reaches its first await; it does not wait for the delayed work.

A workflow that rethrows to its caller

main.js

javascript
function delayResult(value, shouldFail = false) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldFail) {
        reject(new Error("Practice failure"));
        return;
      }

      resolve(value);
    }, 600);
  });
}

let isRunning = false;

async function runSequence(failAt = "none") {
  isRunning = true;
  console.log("Sequence started", isRunning);

  try {
    const user = await delayResult(
      { id: 7, name: "Maya" },
      failAt === "profile"
    );
    console.log("Profile ready", user);

    const greeting = await delayResult(
      `Hello, ${user.name}!`,
      failAt === "greeting"
    );
    console.log("Greeting ready", greeting);
  } catch (error) {
    console.error("Sequence interrupted", error.message);
    throw error;
  } finally {
    isRunning = false;
    console.log("Cleanup complete", isRunning);
  }
}

function reportFailure(error) {
  console.error("Sequence failed:", error.message);
}

runSequence("greeting").catch(reportFailure);
console.log("Caller continues");
Expected output
Sequence started true
Caller continues
Profile ready { id: 7, name: "Maya" }
Sequence interrupted Practice failure
Cleanup complete false
Sequence failed: Practice failure

Browser consoles may style error entries differently, but the observable event order is the same.

Rethrow versus handle locally

Decide whether the failure stays local

Inside runSequence, the catch block has already logged Sequence interrupted. Which statement lets the caller's terminal handler receive the same failure?

Apply the terminal handler

Complete the caller boundary

The workflow has already logged its interruption, rethrown the error, and run its finally cleanup. Complete the terminal handler so it reports the rejection reason. Then predict the final two console events for this greeting-failure call.

javascript
function reportFailure(error) {
  console.error("Sequence failed:", );
}

runSequence("greeting").catch(reportFailure);

Step 9 of 9

Verify the Complete Async Workflow

Complete and locally verify a dependent asynchronous workflow with reliable failure handling and cleanup.

Assemble the established patterns

One workflow, three outcomes

Integrate one local workflow

This final practice combines the patterns you have already used—without networking or interface features.

Your workflow should:

  1. start with isRunning set to true;
  2. await the profile before creating the greeting input;
  3. log and rethrow a failure from either awaited operation;
  4. restore isRunning in finally; and
  5. let the caller’s terminal handler report an escaping rejection.

Use one call per page reload. That keeps each console trace about one workflow, rather than several overlapping runs.

Trace the complete path

A flow diagram where profile work feeds greeting work, while success and both failure routes converge on cleanup; failures then reach a caller boundary.

The greeting depends on the profile result. Cleanup is shared by every outcome, while only failures continue to the caller’s rejection handler.

Tip

Observation rule

Keep the browser console open. Console object formatting can vary slightly by browser, but the labels, their relative order, and the important values should match your prediction.

Run and compare all three cases

index.html

Keep this entry file in the Vite project root.

html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Async Practice</title>
  </head>
  <body>
    <script type="module" src="./main.js"></script>
  </body>
</html>

main.js: run one case per reload

Replace main.js with this complete version. Start with failAt set to "none". Then change only that value to "profile" and "greeting", reloading once for each case.

javascript
function delayResult(value, shouldFail = false) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldFail) {
        reject(new Error("Practice failure"));
        return;
      }

      resolve(value);
    }, 600);
  });
}

let isRunning = false;

async function runSequence(failAt = "none") {
  isRunning = true;
  console.log("Sequence started", isRunning);

  try {
    const user = await delayResult(
      { id: 7, name: "Maya" },
      failAt === "profile"
    );
    console.log("Profile ready", user);

    const greeting = await delayResult(
      `Hello, ${user.name}!`,
      failAt === "greeting"
    );
    console.log("Greeting ready", greeting);
  } catch (error) {
    console.error("Sequence interrupted", error.message);
    throw error;
  } finally {
    isRunning = false;
    console.log("Cleanup complete", isRunning);
  }
}

function reportFailure(error) {
  console.error("Sequence failed:", error.message);
}

const failAt = "none";
runSequence(failAt).catch(reportFailure);
console.log("Caller continues");
Expected output
Sequence started true
Caller continues
Profile ready { id: 7, name: "Maya" }
Greeting ready Hello, Maya!
Cleanup complete false

The output field shows the exact trace for the current `"none"` run. Object formatting may vary by browser. Expected sequence after selecting `"profile"` and reloading (this heading is not a console line): `Sequence started true`, `Caller continues`, `Sequence interrupted Practice failure`, `Cleanup complete false`, then `Sequence failed: Practice failure`. Expected sequence after selecting `"greeting"` and reloading (this heading is not a console line): `Sequence started true`, `Caller continues`, `Profile ready` with Maya’s profile, `Sequence interrupted Practice failure`, `Cleanup complete false`, then `Sequence failed: Practice failure`. Neither failure case should show an unhandled-rejection message because `.catch(reportFailure)` handles the escaping rejection.

Three-case verification

Run exactly one selector per reload. Use this as a compact checklist while you compare the console trace.

Expected behavior for one invocation of the workflow.
SelectorDelayed-work resultRequired checks
noneProfile fulfills, then greeting fulfillsGreeting is `Hello, Maya!`; cleanup logs `false`.
profileProfile rejects; greeting does not startNo readiness label; cleanup precedes the caller failure report.
greetingProfile fulfills; greeting rejectsNo `Greeting ready` label; cleanup precedes the caller failure report.

Reconstruct the critical lines

Complete the async control flow

Complete the critical control-flow elements. Then use the complete main.js from the previous screen to run the baseline and both failure selectors separately.

javascript
async function runSequence(failAt = "none") {
  isRunning = true;
  console.log("Sequence started", isRunning);

  try {
    const user = await delayResult(
      { id: 7, name: "Maya" },
      failAt === "profile"
    );
    console.log("Profile ready", user);

    const greeting = await delayResult(
      `Hello, ${user.name}!`,
      failAt === "greeting"
    );
    console.log("Greeting ready", greeting);
  } catch (error) {
    console.error("Sequence interrupted", error.message);
    
  } finally {
    isRunning = ;
    console.log("Cleanup complete", isRunning);
  }
}

runSequence("greeting").(reportFailure);

Confirm the complete workflow

Report your three-case verification

After running all three selectors on separate reloads, record what you observed in the browser console. Include:

  • the final greeting value for the successful case;
  • whether the greeting operation ran after a profile failure;
  • whether a successful greeting label appeared after a greeting failure;
  • the isRunning value logged at cleanup in every case; and
  • what reported each failure, including whether you saw an unhandled-rejection message.

Use the exact console labels in your response. No file submission is required.

Write at least 180 characters (0/180).

Summary

Promise to await to cleanup

  • A promise represents an eventual result; await supplies its fulfilled value to the dependent next operation.
  • A rejected awaited operation skips later statements in its protected sequence, and a rethrow lets the caller handle that rejection.
  • finally provides one cleanup point that restores temporary state after success or failure.
  • Run independent local cases one at a time so each console trace describes a single workflow.

Download the App now to have access to + 5000 free courses, exercises, certificates and lots of content without paying anything!

  • 100% free online courses from start to finish

    Thousands of online courses in video, ebooks and audiobooks.

  • More than 60 thousand free exercises

    To test your knowledge during online courses

  • Valid free Digital Certificate with QR Code

    Generated directly from your cell phone's photo gallery and sent to your email

Cursa app on the ebook screen, the video course screen and the course exercises screen, plus the course completion certificate