Skip to content

JavaScript Tutorial The Fetch API

A Practical fetch() Helper

Lesson 158 of 258 2 min read By Deepak Paswan Updated Oct 4, 2026

Real projects repeat the same fetch steps: send the request, check the status, parse JSON, handle failures. Put them in one helper.

api.js
JS
export class HttpError extends Error {
  constructor(status, message) {
    super(message);
    this.name = "HttpError";
    this.status = status;
  }
}

export async function request(url, { method = "GET", data, timeout = 8000, headers = {} } = {}) {
  const options = {
    method,
    headers: { Accept: "application/json", ...headers },
    signal: AbortSignal.timeout(timeout)
  };

  if (data !== undefined) {
    options.headers["Content-Type"] = "application/json";
    options.body = JSON.stringify(data);
  }

  let response;
  try {
    response = await fetch(url, options);
  } catch (error) {
    if (error.name === "TimeoutError") throw new Error("The server took too long to answer.");
    throw new Error("Could not reach the server.");
  }

  if (!response.ok) {
    throw new HttpError(response.status, `Request failed (${response.status})`);
  }

  if (response.status === 204) return null;               // no content
  return response.json();
}

export const getJSON = (url, options) => request(url, options);
export const postJSON = (url, data, options) => request(url, { ...options, method: "POST", data });

Using it

script.js
JS
import { getJSON, postJSON, HttpError } from "./api.js";

try {
  const users = await getJSON("/api/users");
  const created = await postJSON("/api/users", { name: "Maya" });
} catch (error) {
  if (error instanceof HttpError && error.status === 404) {
    showMessage("Not found.");
  } else {
    showMessage(error.message);
  }
}

The same logic, runnable

The helper depends on a real server, so here is the central idea, response checking and JSON parsing, as a function that takes any fetch-like function. It lets us test every case without a network.

script.js
JS
async function requestWith(fetchFn, url) {
  let response;
  try {
    response = await fetchFn(url);
  } catch {
    throw new Error("Could not reach the server.");
  }
  if (!response.ok) {
    throw new Error(`Request failed (${response.status})`);
  }
  return response.status === 204 ? null : response.json();
}

const fakes = {
  ok:      async () => new Response('{"name":"Maya"}'),
  missing: async () => new Response("", { status: 404 }),
  empty:   async () => new Response(null, { status: 204 }),
  offline: async () => { throw new TypeError("Failed to fetch"); }
};

async function main() {
  for (const [name, fn] of Object.entries(fakes)) {
    try {
      console.log(name, "→", JSON.stringify(await requestWith(fn, "/api")));
    } catch (error) {
      console.log(name, "→", error.message);
    }
  }
}
main();

The loop prints ok → {"name":"Maya"}, missing → Request failed (404), empty → null and offline → Could not reach the server. Passing the function in (dependency injection) is also how you test code that calls the network.

Design choices explained

  • One place for status checks, JSON parsing and timeouts.
  • A custom error type (HttpError) carries the status code, so callers can react to 404 or 401 specifically. See Custom Errors.
  • Friendly messages for network failures.
  • A timeout by default, so a stalled server does not leave the interface waiting forever.
  • It is a module (Modules), so any file can import it.
info
Libraries
Libraries such as Axios, ky or ofetch wrap fetch in a similar way. For many projects, a helper like this is all you need.