On this page

Fetch API

12 min read TextCh. 4 — Asynchrony

What is the Fetch API?

fetch() is the browser's native API for making HTTP requests. It replaces the old XMLHttpRequest with a promise-based, cleaner and more modern interface.

const response = await fetch(url, options);

It returns a Response that contains the HTTP status, headers, and methods for reading the body.

HTTP methods

Method Use Has body?
GET Retrieve data No
POST Create a resource Yes
PUT Replace a resource Yes
PATCH Partially update Yes
DELETE Delete a resource Generally no

The Response object

The fetch response has important properties and methods:

Property/Method Description
response.ok true if status is 200-299
response.status HTTP code (200, 404, 500, etc.)
response.statusText Status text ("OK", "Not Found")
response.headers Response headers
response.json() Reads the body as JSON (promise)
response.text() Reads the body as text (promise)
response.blob() Reads the body as Blob (promise)

Important note about .ok

fetch does not reject the promise on HTTP errors (404, 500, etc.). It only rejects on network errors (no connection, DNS failure). Always check response.ok:

const res = await fetch('/api/data');
if (!res.ok) {
  throw new Error(`HTTP Error: ${res.status}`);
}
const data = await res.json();

Headers and body

To send data, configure the headers and body:

const res = await fetch('/api/items', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer token123',
  },
  body: JSON.stringify({ name: 'Item', price: 42 }),
});

The body is serialized with JSON.stringify(). The headers tell the server the format of the data.

AbortController

Allows you to cancel in-flight requests. It is essential for:

  • Search fields — Cancel the previous request when typing
  • Navigation — Cancel pending requests when changing pages
  • Timeouts — Cancel requests that take too long
const controller = new AbortController();
fetch(url, { signal: controller.signal });
controller.abort(); // cancels the request

URLSearchParams

For building query strings safely (with automatic encoding):

const params = new URLSearchParams({
  search: 'hello world',
  page: '1',
});
console.log(params.toString()); // "search=hello+world&page=1"

Reusable wrapper

In real projects, create a wrapper function that centralizes common logic: base URL, default headers, error handling, and serialization.


Practice

  1. Make a GET with validation: Write an async function fetchData(url) that performs a fetch GET, checks response.ok, and returns the data parsed as JSON. Handle HTTP errors by throwing an Error with the status code.
  2. Send data with POST: Create a function submitForm(url, data) that performs a fetch POST with Content-Type: application/json and the body serialized with JSON.stringify. Check the response and return the result.
  3. Build a URL with parameters: Use URLSearchParams to build a URL with the parameters search, page, and limit. Print the complete URL to the console.

In the next lesson we will learn about ES modules to organize our code.

fetch does not throw on HTTP 4xx/5xx
Unlike other libraries (such as Axios), fetch only rejects the promise on network errors. 404 or 500 responses are considered successful. Always check response.ok before processing the data.
AbortController for search fields
In search fields, cancel the previous request before making a new one. This prevents race conditions where an old response arrives after a new one.
javascript
// Basic GET
async function getUsers() {
  const response = await fetch('https://api.example.com/users');

  // Check HTTP status
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }

  const users = await response.json();
  return users;
}

// POST - send data
async function createUser(data) {
  const response = await fetch('https://api.example.com/users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(data),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.message);
  }

  return response.json();
}

// PUT - update
async function updateUser(id, data) {
  return fetch(`https://api.example.com/users/${id}`, {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });
}

// DELETE
async function deleteUser(id) {
  const res = await fetch(`https://api.example.com/users/${id}`, {
    method: 'DELETE',
  });
  if (!res.ok) throw new Error('Could not delete');
}

// Usage
const created = await createUser({
  name: 'Maria',
  email: '[email protected]',
});
console.log('Created:', created);
javascript
// AbortController - cancel requests
const controller = new AbortController();

async function search(term) {
  try {
    const res = await fetch(`/api/search?q=${term}`, {
      signal: controller.signal,
    });
    return res.json();
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('Request cancelled');
    } else {
      throw error;
    }
  }
}

// Cancel the request
controller.abort();

// Query parameters with URLSearchParams
function buildURL(base, params) {
  const url = new URL(base);
  const searchParams = new URLSearchParams(params);
  url.search = searchParams.toString();
  return url.toString();
}

const url = buildURL('https://api.example.com/products', {
  category: 'tech',
  sort: 'price',
  page: '1',
  limit: '20',
});
// https://api.example.com/products?category=tech&sort=price&page=1&limit=20

// Reusable wrapper
async function api(endpoint, options = {}) {
  const config = {
    headers: { 'Content-Type': 'application/json' },
    ...options,
    body: options.body ? JSON.stringify(options.body) : undefined,
  };

  const response = await fetch(`/api${endpoint}`, config);

  if (!response.ok) {
    const error = await response.json().catch(() => ({}));
    throw new Error(error.message || `HTTP ${response.status}`);
  }

  return response.json();
}

// Using the wrapper
const users = await api('/users');
const post = await api('/posts', {
  method: 'POST',
  body: { title: 'New post', content: '...' },
});