Skip to lesson content
Web Technologies › Level 18
LEVEL 18 · ASYNC

Asynchronous JavaScript and APIs

Handle waiting, failures and competing responses with clear, owned outcomes.

📦 Promises📨 Fetch + JSON🧭 Request ownership🧪 Lab + MCQs
01

Learning Objectives

Level 17 handled a local form synchronously. A real page may need to wait for a response while the learner keeps using it. This lesson introduces promises, async/await, HTTP responses, JSON parsing, cancellation and result ownership.

Our task is a workshop-catalog loader with clear idle, loading, success, empty, error and canceled outcomes. You will distinguish a network failure from an HTTP error, validate parsed data before rendering and prevent an older response from replacing a newer request’s result.

Async flow

Explain when code waits and when other work can proceed.

Responses

Check HTTP status before interpreting a body.

Data contract

Separate valid JSON from acceptable workshop records.

Ownership

Let only the newest request update the current result.

02

Asynchronous Work and the Event Loop

JavaScript code in a browser context generally runs each job to completion before another job runs on that same thread. Browser facilities can handle waiting work such as timers and network operations, then arrange for callbacks or promise reactions to run later.

Asynchronous does not mean every line runs simultaneously or that heavy JavaScript computation automatically moves to another thread. A long synchronous loop can still block interaction and rendering. Awaiting a promise yields control from the async function while it is suspended; it does not freeze the whole browser.

Promise reactions run as microtasks. A zero-delay timer queues later work; it does not guarantee that its callback runs immediately or at an exact wall-clock time.

console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
// In this simple example: A, D, C, B

The ordinary logs run first. The promise reaction is processed before the later timer task in this example. Do not generalize this small trace into a promise about the timing of every network response.

03

Callbacks — A Starting Point

A callback is a function supplied to another operation. Event listeners from the previous lesson are callbacks. A timer callback receives control after the timer becomes eligible and the event loop can run it.

Not every callback is asynchronous: Array.map calls its callback synchronously. Judge the operation’s contract rather than assuming that passing a function always schedules it for later.

Several nested dependent callbacks can make success and failure paths difficult to follow. Promises provide a composable representation of an eventual outcome.

setTimeout(() => {
  console.log("Timer callback ran");
}, 500);

const doubled = [1, 2].map(value => value * 2);
// map finishes synchronously here: [2, 4]

The 500ms value is a scheduling delay, not an exact observed duration. A busy page or background-tab policy can delay when the callback actually runs.

04

Promises — Pending, Fulfilled or Rejected

A promise represents an eventual result. It begins pending and can settle as fulfilled with a value or rejected with a reason. Once settled, its outcome does not switch from fulfillment to rejection or vice versa.

Calling resolve can adopt another promise’s outcome; resolution and immediate fulfillment are therefore not always the same moment. For beginner examples, focus on the observable eventual fulfillment or rejection.

The Promise constructor’s executor runs synchronously. Use the constructor when adapting a callback operation, rather than wrapping an already promise-based fetch call in an unnecessary new Promise.

function wait(milliseconds) {
  return new Promise(resolve => {
    setTimeout(resolve, milliseconds);
  });
}
wait(500).then(() => console.log("Wait completed"));

The executor schedules a timer immediately; the returned promise remains pending until that timer callback resolves it. The local lab adapts timers into a cancelable simulated response.

05

then, catch and Returning the Next Step

Then returns a new promise. Its callback can return an ordinary value, return another promise, or throw an error. Returning the next asynchronous step makes the chain wait for it and carries failures along the chain.

Catch handles a rejection from the chain before it. Returning a fallback value from catch can turn the later chain into a fulfilled result. Throwing again preserves a failure path for later handling.

A missing return can let a chain proceed before an inner operation finishes. Keep the relationship between stages explicit rather than nesting an unreturned promise and expecting the outer chain to wait.

fetch("/api/workshops")
  .then(response => {
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  })
  .then(data => console.log(data))
  .catch(error => console.error(error.message));

The /api/workshops path is a hypothetical endpoint used in teaching examples; a project must provide that endpoint before using this code. The lab uses local predefined responses for predictable practice.

06

async and await — Read the Sequence Clearly

An async function returns a promise. Returning a normal value fulfills its returned promise with that value; an uncaught error rejects it. Await suspends the function until the awaited value’s promise behavior settles and then supplies the fulfilled value or throws the rejection reason.

Await makes dependent steps readable without making them synchronous browser operations. Code outside the suspended function can continue to run. An async function can still execute substantial synchronous work before or between awaits.

A try/catch around an awaited operation can handle its rejection. Calling a promise-returning operation without awaiting or returning it can leave a failure outside that intended path.

async function loadCatalog() {
  const response = await fetch("/api/workshops");
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  const data = await response.json();
  return data;
}

Fetch supplies a response object and json supplies parsed body data in two asynchronous stages. A response variable is not already the catalog array.

07

try, catch and finally — Outcome and Cleanup

Use try for the intended operation, catch for the failure path and finally for cleanup that should run after either outcome. Finally is suitable for clearing a timer or releasing a request’s resources.

A finally block can still alter control flow if it returns or throws, potentially overriding an earlier result. Keep cleanup free of an unnecessary returned value or new error.

Cleanup must also respect result ownership. An older request’s finally block should not turn off the newer request’s loading state merely because the older operation has ended.

setLoading(true);
try {
  const data = await loadCatalog();
  showCatalog(data);
} catch (error) {
  showError(error.message);
} finally {
  setLoading(false);
}

This simple example assumes one active request. The later ownership example adds an ID guard before changing the UI in finally. Handle expected cancellation separately from a request error that the learner should retry.

08

HTTP and an API Response Contract

An API exposes an agreed interaction: endpoint, method, headers, request data and response format. A client should know which fields and types the response is expected to contain. HTTP status and JSON structure answer different questions.

A 200-range status commonly represents HTTP success. A 404 response means the requested resource was not found; a 500 response indicates a server-side error class. The appropriate interface response depends on the API’s documented contract.

An empty JSON array can be a successful result with no workshops. A malformed body or unacceptable record shape is a different outcome. A 204 response has no content, so do not blindly demand a JSON body for every successful status.

Separate response decisions
Transport

Did the client receive a response or encounter a failure?

HTTP status

Does the status meet the operation’s contract?

Body format

Can the promised representation be parsed?

Record contract

Are the parsed fields and values acceptable?

09

Fetch — Check the Response Before Parsing

Fetch returns a promise for a Response. HTTP error statuses such as 404 do not normally reject that promise by themselves. Check response.ok or the expected status, then choose how to handle the response.

A network or browser access failure can reject fetch without a usable response. An application can also fail later during body reading, parsing or validation. Keep these stages distinct while debugging.

Response.ok is true for statuses 200 through 299. A body-reading method such as json is asynchronous and normally consumes the body stream. Do not assume you can read the same body repeatedly without cloning or another deliberate strategy.

const response = await fetch("/api/workshops", {
  headers: { Accept: "application/json" }
});
if (!response.ok) {
  throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();

This request header communicates a preferred response representation; it does not force the server to return valid JSON or the expected fields. The next step still needs validation.

10

JSON Parsing Is Not Schema Validation

Json parses a response body into a JavaScript value. Valid JSON can represent a string, number, null, object or array. Parsing successfully does not prove that a value is the catalog you expected.

Our catalog contract is a dense array with at most 20 records. Each record has a unique non-empty string ID of at most 40 characters, a non-empty title of at most 80 characters, and integer capacity/reserved values from 0–1000 with reserved not exceeding capacity.

IDs and titles are trimmed before uniqueness and rendering checks. Reject the whole payload if the contract fails. Avoid displaying an invalid availability count or silently turning an absent capacity into zero. Derive remaining only from accepted numeric fields.

if (!Array.isArray(data)) {
  throw new Error("Expected a workshop array");
}
for (const workshop of data) {
  if (!Number.isInteger(workshop.capacity) ||
      !Number.isInteger(workshop.reserved) ||
      workshop.reserved > workshop.capacity) {
    throw new Error("Invalid workshop record");
  }
}

This abbreviated snippet illustrates the idea. The lab’s validator also checks lengths, lower/upper bounds, uniqueness and sparse slots before rendering its predefined payload.

11

UI States — Loading Is Part of the Task

A useful loader begins idle, shows loading while work is pending, and ends with success, empty, error or canceled. Clear stale results when starting a new request, or deliberately label them as old if the product chooses to retain them.

Use a concise status and an appropriate busy state for the result region. An empty result should explain that no workshops were returned; it should not look like an unfinished loading screen. Keep a retry action available after a recoverable failure.

The lab keeps Load enabled so you can start overlapping requests deliberately. Cancel applies to the newest pending request. Its request log explains earlier completions that were ignored.

Loading

A pending response is owned by the newest request.

Success / empty

Accepted records or a clear zero-record outcome.

Error

HTTP, transport, parsing or schema failure.

Canceled

The active request was intentionally aborted.

12

AbortController and Timeouts

AbortController provides a signal that compatible operations can observe. Pass its signal to fetch, then call abort when the result is no longer needed. The operation can reject with an AbortError; cancellation is different from reporting a server failure.

Aborting does not guarantee that a server never received or processed a request. For a real write operation, retry and cancellation policies need the server’s documented behavior. A read-only catalog has different consequences from creating a payment or reservation.

A timeout can be implemented by aborting after a chosen deadline and clearing the timer in finally. Record whether the deadline or the learner initiated cancellation if those outcomes need different messages.

const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
  const response = await fetch("/api/workshops", {
    signal: controller.signal
  });
  // Check status, parse and validate here.
} finally {
  clearTimeout(timer);
}

The lab uses an AbortController with a local timer-backed response. Its Cancel control demonstrates signal-based rejection without making a network request.

13

Overlapping Requests — Newest Result Owns the UI

A slow request can finish after a later, faster request. Without an ownership rule, the older result can overwrite the learner’s more recent choice. Speed alone is not a reliable ordering policy.

Give each request an increasing ID. Before writing success, empty, error or cleanup state, verify that it still owns the current UI. Canceling old work can save resources, but ownership checks remain valuable when an operation cannot be canceled or is already completing.

The lab intentionally allows older requests to continue when a new one starts. Their completions are logged as ignored, while only the newest request can change the current result. Reset and page exit abort pending work and invalidate its ownership.

const requestId = ++latestRequestId;
try {
  const data = await loadCatalog();
  if (requestId !== latestRequestId) return;
  showCatalog(data);
} finally {
  if (requestId === latestRequestId) setLoading(false);
}

Guard failures and finally as well as success. An obsolete request should not replace a newer error message or turn off its busy state.

14

Sequential Work, Promise.all and allSettled

Await dependent steps in sequence when the second needs the first’s result. Start independent operations before awaiting the group if they can run concurrently. Promise.all accepts an iterable of values/promises and fulfills with results in input order, not completion order.

Promise.all rejects when an input rejects; that does not automatically cancel other already-started operations. Promise.allSettled waits for every input to settle and reports each outcome. Choose the behavior that fits the task.

Avoid a forEach(async ...) loop when the caller expects a completed batch before continuing. Use a suitable for...of loop for sequential work or map to create promises followed by an explicit group await.

const catalogPromise = loadCatalog();
const noticePromise = loadNotices();
const [catalog, notices] = await Promise.all([
  catalogPromise, noticePromise
]);

These helper names illustrate two independent API tasks. Their endpoints and failure policies must be implemented by the application. A group await changes coordination, not the server’s capacity or authorization rules.

15

CORS, Credentials and Safe Rendering

Cross-origin browser requests are governed by access rules such as CORS. The server supplies the appropriate response headers; client JavaScript cannot simply grant itself permission. A no-cors request can yield an opaque response that the script cannot read as ordinary JSON, so it is not a repair for an API that needs readable data.

Credentials handling depends on the request options, origins and server policy. Keep private API keys and server secrets out of downloadable frontend files. A browser-visible key is not made private by placing it in an external JavaScript file.

Treat response text as data when rendering labels. Create elements and use textContent rather than inserting server-provided strings as raw HTML. Keep failures understandable without exposing internal server traces to a learner.

const title = document.createElement("h4");
title.textContent = acceptedWorkshop.title;
card.append(title);

The local lab renders only accepted records with textContent and uses predefined response scenarios. It does not contact external endpoints or store catalog data between visits.

16

Premium Visualizer — Async Response Journey

Follow one fixed successful response through loading, status, parsing, validation and rendering. Use the approved timeline and playback controls. The live lab separately exercises success and failure paths with local simulated responses.

ASYNC API RESPONSE TRACE
Step 1 of 5
STEP 01

Start and own a catalog request

Assign a request ID and show loading while the promise is pending.

requestId = ++latestRequestId

What is happening?

Only this request may write the current result while it remains the latest owner.

17

Premium Interactive — Async Catalog Loader

This is a local response simulation. It uses real promises, timers, AbortController and Response.json with predefined bodies; it does not call a live API. Choose a scenario and delay, then Load. Inspect the state, accepted cards, payload and request log.

Try success, an empty catalog, HTTP 404, network failure, malformed JSON and invalid record shape. Cancel while the newest request is loading. Reset cancels pending work and returns to idle. The timer delay is a teaching setting, not an exact timing guarantee.

For a race, start slow success at 1500ms, then switch to fast empty at 500ms and Load again. The newest empty result should remain after the earlier success completes. The log identifies the obsolete result as ignored. You can also start a fast earlier request and a slow newer one to check that old cleanup does not clear the newer loading state.

Idle. Choose a simulated response and load it.

Accepted catalog cards

    Accepted payload

    No accepted payload yet.

    Request log

    []
    18

    Debugging — Locate the Failed Stage

    Pending

    Check whether the awaited operation ever settles and whether cancellation is wired.

    Response

    Separate a rejected request from a received error status.

    Data

    Inspect body parsing and the accepted record contract.

    Ownership

    Check every UI write, including catch and finally.

    If a 404 still reaches a success callback, inspect response.ok rather than assuming fetch rejects every unsuccessful HTTP status. If JSON parsing fails, inspect the body and representation; the server may have returned an HTML error page. If an older result replaces a newer query, check its request ID before rendering.

    If loading disappears while the latest request is pending, inspect an older finally block. If cancellation leaves an unhandled rejection, make sure the canceled operation still has a rejection handler. If a CORS failure appears, inspect the server’s access policy rather than attempting to read an opaque no-cors response.

    Use the deterministic lab cases to isolate one stage. A malformed body should fail parsing; an over-reserved record should fail validation. Neither should leave accepted cards visible under the new error state.

    19

    Interview Questions — Flip to Explain

    QUESTION

    Does async automatically move heavy code off the main thread?

    Click or press Enter to explain
    ANSWER

    No. An async function can still perform blocking synchronous work. Await suspends that function while an awaited promise is pending.

    QUESTION

    What does an async function return?

    Click or press Enter to explain
    ANSWER

    A promise. A returned value fulfills it; an uncaught error rejects it.

    QUESTION

    Does fetch reject just because HTTP status is 404?

    Click or press Enter to explain
    ANSWER

    Normally no. It can fulfill with a Response whose ok value is false. Inspect the status explicitly.

    QUESTION

    Why await response.json separately?

    Click or press Enter to explain
    ANSWER

    Reading and parsing the response body is asynchronous and can fail independently of receiving the response.

    QUESTION

    Does valid JSON prove the catalog record shape is valid?

    Click or press Enter to explain
    ANSWER

    No. Validate types, bounds, unique IDs and relationships according to the application contract.

    QUESTION

    Why guard finally with the request ID?

    Click or press Enter to explain
    ANSWER

    An older request must not clear a newer request’s loading state or change its current UI outcome.

    QUESTION

    Does Promise.all cancel other inputs when one rejects?

    Click or press Enter to explain
    ANSWER

    No. Rejection of the group does not automatically cancel already-started operations.

    QUESTION

    What should an empty successful response show?

    Click or press Enter to explain
    ANSWER

    A clear zero-record result and a finished loading state, rather than an error or perpetual spinner.

    20

    MCQ Practice — Predict the Async Outcome

    PRACTICE

    1. What does an async function return?

    PRACTICE

    2. Which order fits the simple A/timer B/promise C/D example?

    PRACTICE

    3. What should a promise chain return when the next step reads JSON?

    PRACTICE

    4. How does fetch normally handle a received HTTP 404?

    PRACTICE

    5. What does response.ok represent?

    PRACTICE

    6. What can response.json() do after a successful HTTP status?

    PRACTICE

    7. What is required after successfully parsing a catalog body?

    PRACTICE

    8. Which API supplies a signal for cancellation?

    PRACTICE

    9. Why can an older finally block cause a UI bug?

    PRACTICE

    10. In what order does Promise.all supply fulfilled group results?

    PRACTICE

    11. What happens to other started operations when Promise.all rejects?

    PRACTICE

    12. What should the latest successful empty catalog show?

    21

    Extra Practice — Reproduce Specific Async Outcomes

    Success

    Load success. Expected: four accepted records with remaining counts 18, 0, 30 and 12; total 60.

    Empty

    Load empty. Expected: no cards, accepted [] and a completed empty status.

    HTTP error

    Load HTTP 404. Expected: status-stage failure; no accepted payload.

    Parse failure

    Load malformed JSON. Expected: parsing failure after the successful response status.

    Contract failure

    Load invalid record shape. Expected: validation rejection before cards are rendered.

    Cancel

    Cancel during the scheduled delay. Expected: canceled state, busy false and no accepted cards.

    Slow old result

    Start slow success then fast empty. Expected: newest empty persists; old success is logged as ignored.

    Old cleanup

    Start fast success then slow empty. Expected: loading stays true for the newer request after the old completion; finally the newest empty appears.

    22

    Quick Revision

    Async: Waiting can yield control; heavy synchronous work still blocks.
    Promise: Track eventual fulfillment or rejection.
    Chain: Return the next promise to connect its outcome.
    Await: Suspend the async function, not the whole browser.
    Fetch: Check status before reading the body.
    Contract: Parsing and validation are separate stages.
    UI: Idle, loading, success, empty, error and canceled are explicit.
    Abort: Handle cancellation and release timers/listeners.
    Ownership: Guard success, failure and cleanup writes.
    Groups: Choose sequential or grouped work deliberately.
    23

    Glossary — Flip to Learn

    TERM

    Promise

    Click to see meaning
    DEFINITION

    Promise

    A representation of an eventual fulfillment value or rejection reason.

    TERM

    Pending

    Click to see meaning
    DEFINITION

    Pending

    The promise has not yet settled into fulfillment or rejection.

    TERM

    Fulfillment

    Click to see meaning
    DEFINITION

    Fulfillment

    Successful settlement with a value.

    TERM

    Rejection

    Click to see meaning
    DEFINITION

    Rejection

    Unsuccessful settlement with a reason.

    TERM

    Await

    Click to see meaning
    DEFINITION

    Await

    An operation that suspends an async function until the awaited outcome is available.

    TERM

    Response

    Click to see meaning
    DEFINITION

    Response

    The Fetch API representation of a received response and its body access.

    TERM

    JSON

    Click to see meaning
    DEFINITION

    JSON

    A text data representation that can be parsed into JavaScript values.

    TERM

    Schema validation

    Click to see meaning
    DEFINITION

    Schema validation

    Checking a parsed value against an expected data contract.

    TERM

    Abort signal

    Click to see meaning
    DEFINITION

    Abort signal

    A signal that compatible work observes to support cancellation.

    TERM

    Request ownership

    Click to see meaning
    DEFINITION

    Request ownership

    A rule deciding which asynchronous operation may update the current UI.

    24

    Final Challenge — A Loader with Owned Outcomes

    Write an asynchronous catalog loader around an implemented endpoint or the lesson’s local response adapter. Show idle/loading/success/empty/error/canceled clearly. Check the HTTP contract, await body parsing and validate accepted records before rendering them as text.

    Use an AbortController for compatible cancellation and increasing request IDs to protect every UI write. Keep older completion and cleanup from replacing the newest result. Reset or page cleanup should cancel pending work and invalidate it without leaving an unhandled rejection.

    Test all six response scenarios, cancellation, reset while pending and both request-order races. Record which stage fails and whether the newest busy state remains correct. Use a timer-controlled test for logic, then inspect real browser timing, controls and accessibility feedback.

    Success criterion: explain the promise path, distinguish transport/HTTP/parse/schema outcomes and demonstrate stable result ownership. Level 19 will introduce browser storage and other web APIs.

    25

    Level 18 Complete?

    Before marking complete, explain an awaited response, handle one failure at each relevant stage and demonstrate cancellation plus both overlapping-request cases. Completion is a local study marker rather than an assessment score.