Register

JavaScript for Beginners

0 of 24 notes read 0%

Reading progress is saved to your INFIRALS account. Sign in to keep this note marked as read.

async and await, fetch and errors

Async JavaScript16 min readLesson 3 of 4

3.1 async and await

An async function always returns a promise. Inside it, await pauses the function until the awaited promise settles, then continues with the fulfilled value. The rest of the page keeps running while the function is paused, so awaiting does not block the browser.

await turns a promise chain into readable steps. Each line after an await runs in order, and a try catch around the awaits handles both synchronous throws and rejected promises. The syntax removed most of the reason to build long then chains by hand.

An async function that throws returns a rejected promise. An async function that returns a value returns a fulfilled promise with that value. Callers can await it or chain a catch, and forgetting both leaves a rejection unhandled.

3.2 Reading the order

The first await pauses only the async function, not the script that called it. That is why after call prints before loaded in the example below: the function reached its first await, returned a pending promise, and the outer script continued.

This behaviour is the asynchronous equivalent of the timer example. Synchronous code runs first, then queued work resumes. Reading an async function as "run until the first await, then leave" makes its output predictable.

javascript
function wait(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function run() {
  console.log("loading");
  await wait(10);
  console.log("loaded");
}

run();
console.log("after call");

Output

loading
after call
loaded

3.3 fetch

fetch(url) sends an HTTP request and returns a promise for a Response object. The promise fulfils as soon as headers arrive, not when the body has been read. Read the body with response.json() for JSON, response.text() for text or response.blob() for binary data; each returns another promise.

The Response object carries status and headers. response.ok is true for statuses 200 to 299. A 404 or 500 still fulfils the fetch promise, so check ok and throw deliberately before parsing. This surprises many beginners: fetch rejects on network failure, not on HTTP error status.

For a POST, pass an options object with method, headers and a JSON string body. Set the content type header so the server parses the body correctly. Never send a secret from browser code; anything in the page is visible to the user.

javascript
const payload = { id: 1, title: "Learn fetch", done: false };
const url = "data:application/json," + encodeURIComponent(JSON.stringify(payload));

async function loadTodo() {
  const response = await fetch(url);
  if (!response.ok) throw new Error("HTTP " + response.status);
  return response.json();
}

loadTodo().then((todo) => {
  console.log(todo.title);
  console.log(todo.done);
});

Output

Learn fetch
false

3.4 Handling errors

A network request can fail at several points: the request never reaches the server, the server returns an error status, the body is not valid JSON, or the data is valid but does not have the expected shape. Each failure deserves its own message when the user can act on it.

Wrap the awaited steps in try catch. Check response.ok before reading the body. Validate the parsed data with a small guard such as Array.isArray, and show an empty state rather than a broken list. The catch block should log the real error for debugging and show a friendly message to the user.

Finally blocks run for both outcomes and are the place to stop a spinner. A common pattern is to set loading true before the request, false in finally, and render the error in catch. The button is re-enabled in finally so a failure does not lock the form.

3.5 Parallel requests with await

Awaiting one request after another makes them sequential, which doubles the wait when the requests are independent. Start both promises first, then await them, or use Promise.all for a list. The requests begin when fetch is called, not when await runs.

For a dashboard with three panels, fire the three fetches, then await all of them and render each result. If one panel may fail without failing the rest, use allSettled. The choice is about the product requirement, not about syntax.

javascript
async function loadBrokenJson() {
  try {
    const response = await fetch("data:application/json,not-json");
    return await response.json();
  } catch (error) {
    console.log("invalid JSON payload");
  }
}

loadBrokenJson();

Output

invalid JSON payload

3.6 AbortController and timeouts

A fetch can be cancelled with an AbortController. Create the controller, pass controller.signal in the fetch options, and call controller.abort() when the operation is no longer needed, such as when the user navigates away or types a new search query.

The aborted fetch rejects with an AbortError. Check error.name === "AbortError" in the catch block so a deliberate cancel is not shown as a failure. Combining the controller with a timer produces a request timeout, a pattern that many libraries wrap in a helper.

Cancellation matters for search boxes. Every keystroke can start a request, and an earlier response that arrives late can overwrite the newer results. Aborting the previous request when a new one starts prevents the stale write.

3.7 Response shapes and validation

response.json() returns a promise for the parsed body. The parsed value can be an object, an array or a primitive, and the code should check which before using it. A small guard such as Array.isArray(data) at the edge prevents confusing errors later in render code.

The API contract may include an envelope, such as { data: [...] } or { error: "..." }. Read the envelope, then pass the inner value to the rest of the code. One function that normalises the response keeps the shape knowledge in one place and makes the dashboard easy to adapt when the API changes.

Validation can be simple checks or a schema library. For this course, a few explicit checks are enough: the value is an array, each item has the expected keys, and optional fields have defaults. The point is to fail with a clear message at the boundary rather than deep inside rendering.

3.8 Caching and retries

A repeated request for the same data can be cached in memory for the session. Store the parsed value and a timestamp in a Map, and reuse it while it is fresh. A cache removes the wait for a second visit to the same panel and reduces load on the API.

Retries help with transient failures, but they need a limit and a delay. Retrying immediately can hammer a failing server, and retrying a 404 will never succeed. Retry network failures and 5xx responses a few times with a growing delay, and surface other errors to the user.

Neither caching nor retries belongs in every fetch call. Add them when the project needs them, and keep them visible in a named helper instead of hiding them inside an API function that looks like a plain request.

3.9 Common mistakes

  • Forgetting that fetch fulfils on a 404 and treating the response as success.
  • Reading the body twice; a response can be consumed only once.
  • Awaiting independent requests one after another.
  • Assuming await blocks the whole page.
  • Leaving a rejected async call without a catch.
  • Trusting the response shape without validating it.

Exam

Async questions mix order and error handling. Split the function at each await: the part before runs immediately, the rest resumes when the promise settles. Then decide whether an HTTP error, a parse error or a rejected promise reaches the catch.

  • Write an async function that awaits a timer and logs after it.
  • Fetch the data URL and log one parsed field.
  • Add an ok check and throw on a bad status.
  • Move two independent awaits into Promise.all and time both versions.