How to Consume REST APIs and Handle Async/Await in Vanilla JavaScript: 7 Patterns from dom-projects

You can consume REST APIs in vanilla JavaScript by using the native Fetch API inside an async function, awaiting the response, checking response.ok for errors, and parsing the JSON payload with await response.json().

The jisan-mia/dom-projects repository demonstrates exactly how to consume REST APIs and handle async/await in vanilla JavaScript without frameworks. These practical examples show a consistent 7-step architectural pattern used across multiple mini-projects, from dad joke generators to Quran verse viewers.

The 7-Step Pattern for Consuming REST APIs

Every project in the repository follows the same asynchronous flow. Here is the breakdown of how to implement it in your own code.

Step 1: Construct the Request URL

Create the endpoint URL using static strings, template literals, or user-generated values. In projects/weather-app/script.js, the code builds dynamic URLs based on user input, while projects/Quran/app.js constructs endpoints like `https://api.alquran.cloud/v1/surah/${number}/en.asad`.

Step 2: Invoke fetch with Options

Call fetch() with optional configuration objects for method, headers, and body. The projects/rest-client/app.js file demonstrates setting custom headers such as "Accept": "application/json", while projects/advanced-todo/script.js shows POST requests with JSON bodies.

Step 3: Await the Response

Declare the surrounding function as async so you can use await to pause execution until the Promise resolves. In projects/dad-jokes/script.js, the code uses const response = await fetch(url) to handle the network round-trip without blocking the main thread.

Step 4: Validate with response.ok

Check the boolean response.ok property or inspect response.status to catch HTTP errors. The projects/rest-client/app.js implementation throws descriptive errors if !response.ok, allowing centralized error handling in a catch block.

Step 5: Parse the JSON Payload

Stream and parse the response body using await response.json(). This pattern appears in projects/random-user/script.js and projects/Quran/app.js, converting the raw network response into usable JavaScript objects.

Step 6: Update the DOM

Render the data by updating innerHTML or creating elements. The projects/advanced-todo/script.js and projects/advanced-quiz-app/script.js files map data arrays to HTML strings and inject them into the page.

Step 7: Wrap in an Async Function

Encapsulate the logic in a named async function or an immediately-invoked async function expression (IIFE) to enable top-level await. The projects/dad-jokes/script.js and projects/rest-client/app.js files follow this pattern by defining async function loadData() and then invoking it immediately.

Practical Implementation Examples

Here are self-contained snippets derived from the repository that demonstrate real-world usage.

Simple GET Request

This example from projects/dad-jokes/script.js fetches random jokes and renders them:

async function getJokes() {
  const url = "https://official-joke-api.appspot.com/random_ten";

  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    const jokes = await response.json();
    renderJokes(jokes);
  } catch (err) {
    console.error("Failed to load jokes:", err);
  }
}

function renderJokes(jokes) {
  const container = document.getElementById("jokes-list");
  container.innerHTML = jokes.map(joke => 
    `<li>${joke.setup} - ${joke.punchline}</li>`
  ).join("");
}

getJokes();

POST Request with JSON Body

Following the pattern in projects/advanced-todo/script.js, this snippet creates a new resource:

async function addTodo(title) {
  const url = "https://jsonplaceholder.typicode.com/todos";

  try {
    const response = await fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify({ title, completed: false })
    });

    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    const saved = await response.json();
    console.log("Created todo:", saved);
  } catch (e) {
    console.error(e);
  }
}

addTodo("Buy groceries");

Parallel Requests with Promise.all

As demonstrated in projects/question-list-progress/script.js, fire multiple requests simultaneously:

async function loadDashboard() {
  const USERS_API = "https://jsonplaceholder.typicode.com/users";
  const TODOS_API = "https://jsonplaceholder.typicode.com/todos";

  try {
    const [usersRes, todosRes] = await Promise.all([
      fetch(USERS_API),
      fetch(TODOS_API)
    ]);

    if (!usersRes.ok || !todosRes.ok) {
      throw new Error("One of the calls failed");
    }

    const users = await usersRes.json();
    const todos = await todosRes.json();

    console.log({ users, todos });
  } catch (e) {
    console.error(e);
  }
}

loadDashboard();

Error Handling Best Practices

The repository demonstrates three critical error handling patterns when you consume REST APIs and handle async/await in vanilla JavaScript.

First, always verify response.ok before parsing. This boolean property checks that the status code falls between 200 and 299. The projects/rest-client/app.js file throws descriptive errors immediately when this check fails.

Second, wrap await calls in try / catch blocks. This captures both network failures (DNS errors, timeouts) and HTTP errors thrown manually after checking response.ok. Every project in the repository uses this structure to prevent unhandled promise rejections.

Third, parse the JSON inside the try block but after validating the response. Since response.json() returns a promise, it can fail if the body is not valid JSON. Keeping it inside the same try block ensures you catch parsing errors alongside network errors.

Summary

  • Use async functions to enable the await keyword and write asynchronous code that reads like synchronous logic.
  • Always check response.ok after awaiting fetch to catch HTTP error statuses before parsing.
  • Parse JSON with await response.json() to convert the stream into usable JavaScript objects.
  • Wrap logic in try / catch to handle network failures, timeouts, and parsing errors gracefully.
  • Fire parallel requests using Promise.all to improve performance when fetching independent resources.
  • Reference the repository at jisan-mia/dom-projects to see working implementations in projects/dad-jokes/script.js, projects/rest-client/app.js, and projects/question-list-progress/script.js.

Frequently Asked Questions

What is the difference between using async/await and promise chaining with .then()?

Async/await provides syntactic sugar that makes asynchronous code look and behave like synchronous code, improving readability. While .then() chaining works perfectly well, it can lead to deeply nested structures often called "promise hell." The jisan-mia/dom-projects repository exclusively uses async/await in files like projects/dad-jokes/script.js because it allows developers to use standard try / catch blocks for error handling and makes the sequence of operations—fetch, check, parse, render—immediately obvious.

How do I handle HTTP error status codes like 404 or 500 when using fetch?

You must manually check the response.ok property or response.status because fetch only rejects on network failure, not HTTP errors. In projects/rest-client/app.js, the code explicitly checks if (!response.ok) and throws a new Error with the status code. This thrown error is then caught by the surrounding try / catch block, allowing you to handle 404 (Not Found) or 500 (Server Error) responses gracefully rather than attempting to parse invalid JSON.

Can I use async/await at the top level of my JavaScript file without wrapping it in a function?

Modern browsers and Node.js support top-level await in ES modules, but for classic scripts, you should wrap the code in an async Immediately Invoked Function Expression (IIFE) or a named async function. The jisan-mia/dom-projects repository uses the named function approach in files like projects/dad-jokes/script.js, where async function loadJokes() is defined and then immediately called with loadJokes(). This pattern ensures compatibility across all environments while keeping the code structure clean and allowing the use of await throughout your logic.

How do I make multiple API calls simultaneously without waiting for each one to finish sequentially?

Use Promise.all() to fire requests concurrently and await all responses together. In projects/question-list-progress/script.js, the code demonstrates this by calling await Promise.all([fetch(USERS_API), fetch(TODOS_API)]), which returns an array of response objects once both fetches complete. This approach significantly improves performance when the requests are independent, reducing total wait time from the sum of individual latencies to the duration of the slowest single request.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →