Chapter 22 of 28 · JavaScript for Beginners — Complete Free Tutorial

Fetch API & Working with APIs

Modern apps are clients: they talk to servers over HTTP and render what comes back. The fetch() API is JavaScript's standard tool for that — promise-based, async-friendly, and everywhere. This chapter covers GET and POST flows, error handling that people actually get right, loading and empty states, and cancellation.

Your first request

async function getJoke() {
  const res = await fetch('https://api.chucknorris.io/jokes/random');
  const data = await res.json();
  console.log(data.value);
}
getJoke();

Three steps: fetch → check status → parse JSON. The parse step returns a promise too, hence the second await.

The response object

const res = await fetch('/api/users');

console.log(res.ok);          // → true (status 200–299)
console.log(res.status);      // → 200
console.log(res.headers.get('content-type'));  // → 'application/json'

const text = await res.text();     // non-JSON bodies
const blob = await res.blob();     // files/images

The error-handling rules (read twice)

async function getUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);

    if (!res.ok) {                          // ← 404/500 do NOT throw!
      throw new Error(`Request failed: ${res.status}`);
    }

    return await res.json();
  } catch (err) {
    // network offline / DNS failure / HTTP error above
    console.error(err.message);
    throw err;
  }
}
  • fetch rejects only on network-level failure (offline, blocked).
  • HTTP errors (404, 500) resolve normally — you must check res.ok.
  • Always return await res.json() (or await it) or you get a pending promise.

POST, PUT, DELETE

const res = await fetch('/api/orders', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`,
  },
  body: JSON.stringify({ sku: 'KB-1', qty: 2 }),
});

if (!res.ok) throw new Error(`HTTP ${res.status}`);
const order = await res.json();

// DELETE — no body needed
await fetch(`/api/orders/${order.id}`, { method: 'DELETE' });

Rules of thumb:

  • JSON bodies need Content-Type: application/json and JSON.stringify.
  • GET requests should have no body.
  • Sending FormData directly? Do not set the header — the browser adds the multipart boundary itself (chapter 17).

Query strings

const params = new URLSearchParams({ q: 'js tutorial', page: 2, tag: 'async' });
const url = `/api/search?${params}`;
// → /api/search?q=js+tutorial&page=2&tag=async

const res = await fetch(url);

Full pattern: loading, success, empty, error

const ui = {
  list: document.querySelector('#results'),
  status: document.querySelector('#status'),
};

async function renderProducts(query = '') {
  ui.status.textContent = 'Loading…';
  ui.list.replaceChildren();

  try {
    const res = await fetch(`/api/products?${new URLSearchParams({ q: query })}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);

    const products = await res.json();

    if (!products.length) {
      ui.status.textContent = 'No products found.';
      return;
    }

    ui.status.textContent = '';
    ui.list.append(...products.map((p) => {
      const li = document.createElement('li');
      li.textContent = `${p.name} — ${p.price}`;
      return li;
    }));
  } catch (err) {
    ui.status.textContent = `Something went wrong: ${err.message}`;
  }
}

Expected output: a spinner while waiting, results on success, a friendly empty state when the array is blank, and an error message on failure — all four states every data-driven UI needs. Note it reuses DOM skills from chapter 15 and async/await from chapter 21.

Cancellation: AbortController

let controller;

async function search(term) {
  controller?.abort();                        // cancel any in-flight request
  controller = new AbortController();

  try {
    const res = await fetch(`/api/search?q=${term}`, { signal: controller.signal });
    const data = await res.json();
    render(data);
  } catch (err) {
    if (err.name === 'AbortError') return;    // expected — ignore silently
    console.error(err);
  }
}

searchInput.addEventListener('input', debounce((e) => search(e.target.value), 300));

Without cancellation, a slow earlier response can overwrite a newer one (the classic race condition in search-as-you-type).

CORS, in plain language

  • Servers may allow only certain origins: Access-Control-Allow-Origin.
  • Blocked by CORS → the browser reports an error even though the URL works in the address bar — the fix is server-side (or a proxy during development).
  • Simple GETs are “simple requests”; those with JSON headers trigger a preflight OPTIONS request first.

Caching briefly

fetch('/api/feed', { cache: 'no-store' });           // never cache
fetch('/api/feed', { headers: { 'If-None-Match': etag } });  // conditional GET

Browsers handle HTTP caching for you; for app-level freshness use cache: 'no-store' or explicit headers — and never cache authenticated data in localStorage carelessly (chapter 25).

Common mistakes

  • Ignoring res.ok — rendering a 404 HTML page as JSON fails later with a confusing parse error.
  • Missing header on JSON POST — server sees form data, rejects with 415/400.
  • Not awaiting .json() — console.log(data) shows Promise {…}.
  • Fire-and-forget requests — every request needs loading, error and empty handling.
  • Using fetch results outside the async function — variables assigned with await do not exist yet when you log synchronously afterwards.
  • Doing auth client-side only — tokens in JS can be read by any script; the server must authorise every request.

Practice checklist

  1. Fetch https://api.github.com/users/{name} and render name + avatar + repo count (remember to check res.ok).
  2. Build the four-state UI (loading / success / empty / error) against a real endpoint.
  3. POST a JSON payload to a test API (e.g., JSONPlaceholder) and log the echoed response.
  4. Add search-as-you-type with debounce + AbortController cancellation.

Your app now talks to servers — but as it grows, one file will not do. Next: JavaScript Modules.

Quiz: Fetch API & Working with APIs

5 questions · 70% to pass

Frequently Asked Questions

JavaScript is the programming language of the web. It adds behaviour to pages: responding to clicks and input, validating forms, fetching data from APIs, updating the DOM without reloading, and much more. It also runs on servers (Node.js), in desktop and mobile apps, and in build tooling.

They are unrelated languages that share a similar name for historical marketing reasons. Java is a statically-typed compiled language; JavaScript is a dynamically-typed interpreted language designed for the browser. Knowing one does not imply knowledge of the other.

Helpful but not mandatory — HTML gives you the structure JavaScript manipulates and CSS the styling it toggles. If you know basic tags, classes and selectors, you have enough to start; the DOM chapters assume only that much.

Use const by default and let when the value must change. Avoid var entirely in modern code: it is function-scoped instead of block-scoped, hoists differently, and can create accidental globals. This course uses const and let throughout.

The Document Object Model is the browser’s in-memory tree of objects representing your HTML. JavaScript reads and modifies that tree — selecting elements, changing text, adding nodes, attaching event listeners — and the browser re-renders the page to match.

== compares after converting types (so 5 == "5" is true), while === requires the same value AND the same type (5 === "5" is false). Loose equality has famously confusing rules — use === and !== everywhere unless you have a specific reason not to.

JavaScript is single-threaded, so long-running work (timers, fetches, events) is handled by the browser and its callbacks are queued. The event loop moves queued callbacks onto the call stack only when it is empty, with promise callbacks (microtasks) taking priority over timers. It is why asynchronous code never freezes the page.

A Promise is an object representing a future value — pending, fulfilled or rejected — that can be chained and combined (Promise.all). async/await is syntax sugar over promises that lets asynchronous code read top-to-bottom with ordinary try/catch error handling.

Yes. Frameworks are built on exactly these fundamentals — components are functions, state is your variable discipline, effects are events, rendering is DOM work. Developers who learn vanilla first understand what the framework is doing and debug far faster.

Type out the examples instead of copying, build small end-to-end projects (expense tracker, weather app, quiz), handle every UI state (loading, empty, success, error), and revisit fundamentals while reading real code. Consistency beats volume — a little most days outpaces occasional marathons.