Fetch API

Learn how to use the Fetch API to make HTTP requests from JavaScript, read responses, and perform GET, POST, PUT, PATCH, and DELETE operations against a server.

Modern web applications rarely load all their data up front. Instead, they talk to servers in the background — fetching a list of products, submitting a form, or updating a record — without reloading the page. The Fetch API is the standard, built-in way to do this in JavaScript. It replaces the older, clunkier XMLHttpRequest with a cleaner, Promise-based interface that works naturally with async/await.

What fetch() does

fetch() is a global function available in every modern browser (and in Node.js since version 18). You call it with a URL, and it sends an HTTP request to that URL and returns a Promise.

That Promise resolves with a Response object as soon as the server sends back the first byte of a response — it does NOT wait for the entire body to download, and it does not automatically parse the body as JSON. You must explicitly ask the Response object to read and parse its body, which is itself an asynchronous operation returning another Promise.

This two-step nature (get the Response, then read the body) trips up many beginners who expect fetch('...').then(data => ...) to hand them data directly. It doesn't — it hands you a Response wrapper first.

The request and response objects

By default, fetch(url) sends a GET request with no body. To send other methods or custom data, you pass a second argument: an options object.

The options object can specify method (a string like 'POST'), headers (an object of header names and values), and body (the data to send, often a JSON string).

The Response object that fetch() resolves with carries useful properties: response.status (the numeric HTTP status code, like 200 or 404), response.ok (a boolean that is true only for status codes 200–299), response.headers (a Headers object you can inspect), and methods like response.json(), response.text(), and response.blob() to read the body in different formats.

• response.status — the numeric HTTP status code (200, 404, 500, ...) • response.ok — true when status is in the 200–299 range • response.headers.get('Content-Type') — read a specific response header • response.json() — parses the body as JSON, returns a Promise • response.text() — reads the body as plain text, returns a Promise

fetch() never rejects on HTTP error status

This is the single most common source of confusion with fetch(). The Promise returned by fetch() only rejects on a network failure — no internet connection, DNS failure, CORS block, and similar. A 404 Not Found or a 500 Internal Server Error is still a completed HTTP exchange, so fetch() resolves normally with a Response whose .ok is false and .status is 404 or 500.

This means a naive .then(response => response.json()).catch(err => ...) will silently try to parse an error page as JSON instead of reporting the failure. Always check response.ok (or response.status) before trusting the body, and throw an Error yourself if it is not ok so your .catch() or try/catch block can handle it.

Sending different HTTP methods

GET requests retrieve data and normally have no body. POST requests create a new resource on the server and typically send data in the body. PUT replaces an existing resource entirely, PATCH updates part of an existing resource, and DELETE removes a resource.

For POST, PUT, and PATCH, you usually set the 'Content-Type' header to 'application/json' and pass JSON.stringify(yourObject) as the body, so the server knows how to interpret the bytes you are sending.

fetch() with async/await

await fetch(url) pauses until the Response object arrives — this does not wait for the body to be fully parsed. response.ok must be checked manually; fetch() does not throw for 4xx/5xx statuses. await response.json() is a second asynchronous step that parses the response body.

async function loadUser(id) {
  const response = await fetch(`https://api.example.com/users/${id}`);

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

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

Code Examples

This is the classic Promise-chain style of using fetch(). The first.then() receives the Response object and checks response.ok before parsing the body as JSON. Returning response.json() from that.then() hands the resolved, parsed data to the next.then(). Any network failure or thrown error anywhere in the chain is caught by the final.catch(). Expected result: Loaded products: [ {id: 1, name: 'Widget',...}, {id: 2, name: 'Gadget',...} ] if (!response.ok) { throw new Error(...); }

Common mistake: Skipping the response.ok check and going straight to response.json() means a 404 error page gets silently 'parsed', often throwing a confusing JSON syntax error instead of a clear HTTP error.

This example sends a POST request with a JSON body. The 'Content-Type' header tells the server how to interpret the body. try/catch wraps the whole async operation, catching both network failures and the manually thrown error for bad status codes. Re-throwing inside the catch lets a caller further up the chain also react to the failure. Expected result: Created user with id: 4821 PUT and PATCH requests follow the exact same pattern — just change the method string and adjust the body to match what the endpoint expects (a full replacement object for PUT, a partial object for PATCH).

body: JSON.stringify({ name, email }) Many DELETE endpoints respond with HTTP 204 No Content, meaning the request succeeded but there is no body to parse. Calling response.json() on an empty body would throw a parsing error, so this example checks for status 204 first and skips body parsing entirely in that case. Expected result: User deleted, no content returned.

if (response.status === 204) { ... }

fetch('https://api.example.com/products')
  .then(response => {
    if (!response.ok) {
      throw new Error('Network response was not ok: ' + response.status);
    }
    return response.json();
  })
  .then(products => {
    console.log('Loaded products:', products);
  })
  .catch(error => {
    console.error('Fetch failed:', error.message);
  });
async function createUser(name, email) {
  try {
    const response = await fetch('https://api.example.com/users', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ name, email })
    });

    if (!response.ok) {
      throw new Error(`Server responded with ${response.status}`);
    }

    const createdUser = await response.json();
    console.log('Created user with id:', createdUser.id);
    return createdUser;
  } catch (error) {
    console.error('Could not create user:', error.message);
    throw error;
  }
}

createUser('Ada Lovelace', 'ada@example.com');
async function deleteUser(id) {
  const response = await fetch(`https://api.example.com/users/${id}`, {
    method: 'DELETE'
  });

  if (response.status === 204) {
    console.log('User deleted, no content returned.');
    return true;
  }

  if (!response.ok) {
    throw new Error(`Delete failed with status ${response.status}`);
  }

  return true;
}

The fetch() request/response cycle

fetch() sends the request and resolves with a Response wrapper; reading that Response (e.g. .json()) and using the resulting data to update the page are separate, later steps.

JavaScript
    |
 fetch()
    |
    v
HTTP Request
    |
    v
Server / API
    |
    v
HTTP Response
    |
    v
JSON / Data
    |
    v
JavaScript
    |
    v
DOM Update

Important Rules

fetch(url) sends a GET request by default; pass an options object as the second argument to change the method, headers, or body.

fetch()'s returned Promise rejects only on network-level failure, never merely because the server returned a 4xx or 5xx status — always check response.ok or response.status.

Reading the body (response.json(), response.text()) is a separate asynchronous step from receiving the Response object itself.

When sending JSON, set the 'Content-Type': 'application/json' header and pass JSON.stringify(data) as the body.

A response with status 204 No Content has no body — do not attempt to parse it as JSON.

Where this shows up

Fetch is the backbone of almost every dynamic feature on the web: infinite-scrolling feeds that load more posts as you scroll, search boxes that query results as you type, dashboards that pull live data, and forms that submit without a full page reload. On EngiLipi itself, fetch() is how the practice compiler pages send your code to a server for execution and display the result back on the page.

fetch(url).then(res => res.json()).then(data => console.log(data));

fetch(url).then(res => { if (!res.ok) throw new Error(res.status); return res.json(); }).then(data => console.log(data));

Without checking response.ok, a 404 or 500 error page is passed straight into .json(), which either throws a confusing parse error or silently returns unexpected error-page data instead of a clear, catchable HTTP error.

fetch(url, { method: 'POST', body: { name: 'Ada' } });

fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Ada' }) });

The body of a fetch request must be a string (or other body type like FormData/Blob) — passing a raw object sends the useless string '[object Object]' unless it is explicitly serialized with JSON.stringify, and the Content-Type header should describe that format.

[Multiple Choice] What does the Promise returned by fetch() do when the server responds with a 404 status? A. It rejects immediately with a 404 error B. It resolves with a Response object whose .ok is false and .status is 404 C. It resolves with null D. It throws a synchronous exception

B. It resolves with a Response object whose .ok is false and .status is 404 — fetch() only rejects on network-level failures. An HTTP error status like 404 is still a completed exchange, so the Promise resolves normally, and you must check response.ok or response.status yourself.

[True / False] response.json() and the Promise from fetch() itself resolve at the same time, with the same data. A. True B. False

B. False — False. fetch() resolves with a Response object once headers arrive; response.json() is a separate, later asynchronous call that reads and parses the body.

Write an async function getPost(id) that fetches https://jsonplaceholder.typicode.com/posts/{id}, throws an Error if the response is not ok, and otherwise returns the parsed JSON. Then write a second function that calls getPost and logs the post's title, catching and logging any error.

async function getPost(id) { const response = await fetch(https://jsonplaceholder.typicode.com/posts/${id}); if (!response.ok) { throw new Error(Failed with status ${response.status}); } return response.json(); } async function showPostTitle(id) { try { const post = await getPost(id); console.log(post.title); } catch (error) { console.error('Could not load post:', error.message); } } showPostTitle(1);

Remember that response.ok must be checked before calling response.json(), and both fetch() and response.json() need to be awaited.