Quick Answer

The JavaScript Fetch API lets you request data from a server directly in the browser. You call fetch(url), await the response, then call response.json() to read the data. Always check response.ok before using the data, because fetch only rejects on network failures, not on 404 or 500 errors. Wrap everything in try/catch with async/await for clean, readable code.

What Is the JavaScript Fetch API?

The JavaScript Fetch API is a built-in browser tool for talking to servers. When you type a username on a website and see the profile load, or you submit a form and get a result without the page reloading, that is usually fetch doing the work behind the scenes.

Before fetch, developers used an older, clunky object called XMLHttpRequest. Fetch replaced it with a cleaner design based on promises, which pair nicely with async/await. It is available in every modern browser, so you do not need to install anything.

In this tutorial we will make GET and POST requests, parse JSON, set headers, handle errors properly, and build a small live example. If you want structured practice alongside this, our free JavaScript course covers these fundamentals step by step.

Your First GET Request

A GET request asks a server for data. The simplest version of fetch takes one argument: the URL you want to call.

async function getPost() {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
  const data = await response.json();
  console.log(data.title);
}

getPost();

Two things are happening here. First, await fetch(...) sends the request and waits for the server to reply with a Response object. Second, await response.json() reads the response body and turns it into a normal JavaScript object.

Notice that both steps use await. Fetch is asynchronous, meaning it does not block the rest of your page while it waits. The async keyword on the function is what lets you use await inside it. Paste this into your browser console and you will see a post title printed out.

Parsing JSON From the Response

Most APIs send data back as JSON (JavaScript Object Notation), which looks like a JavaScript object written as text. The response you get from fetch does not hand you that data directly. It gives you a Response object, and you have to read the body yourself.

The most common method is response.json(). It reads the full body and parses the JSON text into a real object or array:

const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
const data = await response.json();

console.log(data.id);     // 1
console.log(data.title);  // the post's title

A key gotcha: response.json() also returns a promise, so it needs its own await. If you forget it, you will log a pending promise instead of your data. If the API sends plain text instead of JSON, use response.text() rather than .json().

Handling Errors and Non-200 Responses

This is the part beginners get wrong most often. Fetch does not throw an error when the server returns a 404 or 500. As long as the server replied at all, fetch considers the request a success. It only rejects the promise when the request could not be made at all, such as no internet or a blocked request.

Here is what actually causes fetch to reject:

SituationDoes fetch reject the promise?
Request succeeds (status 200)No
404 Not FoundNo
500 Server ErrorNo
No internet / network failureYes
Blocked by CORSYes

Because of this, you must check the response yourself. Every Response has an ok property that is true only for status codes in the 200 range, plus a status number.

async function getPost(id) {
  try {
    const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);

    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }

    const data = await response.json();
    return data;
  } catch (error) {
    console.error('Fetch failed:', error.message);
  }
}

The try/catch handles real network failures, and the if (!response.ok) check handles bad status codes. Together they cover both cases.

POST Requests: Sending Data With Headers

A GET request reads data. A POST request sends data, for example creating a new user or saving a comment. To do that, you pass a second argument to fetch: an options object.

async function createPost() {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      title: 'Learning Fetch',
      body: 'This is my first POST request.',
      userId: 1
    })
  });

  const data = await response.json();
  console.log(data);
}

createPost();

Three parts matter here. method: 'POST' tells the server you are sending data. The headers object sets Content-Type to application/json so the server knows to expect JSON. And body holds the actual data.

The most common mistake is forgetting JSON.stringify(). The body must be a string, not a plain object. JSON.stringify() converts your object into JSON text before it is sent. Headers are also where you would add an API key or an auth token when a real API requires one.

A Small Live-Data Example

Let us put it all together with real, live data. GitHub has a public API that returns any user's profile, no key needed. This function fetches a user and displays their name, avatar, and repo count in a page element.

async function showUser(username) {
  const box = document.getElementById('result');
  box.textContent = 'Loading...';

  try {
    const response = await fetch(`https://api.github.com/users/${username}`);

    if (!response.ok) {
      throw new Error(`User not found (status ${response.status})`);
    }

    const user = await response.json();

    box.innerHTML = `
      <img src="${user.avatar_url}" width="80" alt="avatar">
      <h3>${user.name || user.login}</h3>
      <p>Public repos: ${user.public_repos}</p>
    `;
  } catch (error) {
    box.textContent = error.message;
  }
}

showUser('torvalds');

You need one line of HTML on the page for this to have somewhere to render: <div id="result"></div>. Try changing 'torvalds' to your own GitHub username. If you type a name that does not exist, the if (!response.ok) check catches the 404 and shows a friendly message instead of a broken page. That is the full pattern: show a loading state, fetch, check the status, use the data, and handle failures.

async/await vs .then() Chains

You will see two styles of fetch code online. The older style uses .then() to chain callbacks; the modern style uses async/await. Both work, but async/await usually reads more like plain steps.

// .then() style
fetch('https://jsonplaceholder.typicode.com/posts/1')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error));

// async/await style (same result)
try {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts/1');
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}

Our recommendation for beginners is to learn async/await. It keeps related lines together, error handling lives in a normal try/catch, and it is far easier to read once you have several requests. You should still understand .then() because you will meet it in older code and tutorials.

Common Gotchas and Final Tips

A few things trip people up over and over. Keep these in mind and you will avoid most fetch bugs:

  • fetch does not reject on 404 or 500. Always check response.ok yourself.
  • You can only read the body once. Calling response.json() twice on the same response fails. Store the result in a variable.
  • Forgetting await on response.json() gives you a pending promise, not your data.
  • Forgetting JSON.stringify() on a POST body sends broken data.
  • CORS errors come from the server's rules, not your code. You cannot fix them purely from the browser.

To recap the whole flow: call fetch, await the response, check response.ok, then await response.json(), all wrapped in try/catch. Once that pattern feels natural, connecting your web pages to real APIs becomes routine. To keep building, work through the projects in our free JavaScript course and practise fetch against a public API of your choice.

Frequently Asked Questions

Why does my fetch not catch a 404 error?

Because fetch treats any completed reply as a success, even a 404 or 500. The promise only rejects on network-level failures like no internet or a blocked request. To catch bad status codes, check response.ok (or response.status) yourself and throw an error when it is false.

Do I need to install anything to use fetch?

No. The Fetch API is built into every modern browser, so you can use it in plain JavaScript with no libraries. Note that older code and some Node.js versions used libraries like axios or node-fetch, but recent Node.js versions also include fetch natively.

What is the difference between response.json() and response.text()?

Both read the response body, but response.json() parses the text as JSON and gives you an object or array, while response.text() gives you the raw string. Use .json() for JSON APIs and .text() when the server returns plain text or HTML. Each returns a promise, so both need await.

Why do I need JSON.stringify() when sending a POST request?

The body of a fetch request must be a string, but your data is usually a JavaScript object. JSON.stringify() converts that object into JSON text so the server can read it. Pair it with a Content-Type: application/json header so the server knows what format to expect.

Should I use async/await or .then() with fetch?

Both work and produce the same result. For beginners we recommend async/await because it reads like step-by-step instructions and uses normal try/catch for errors. Learn .then() too, since you will run into it in older tutorials and codebases.

What causes a CORS error with fetch?

CORS (Cross-Origin Resource Sharing) errors happen when a server does not allow requests from your website's origin. It is a server-side rule, not a bug in your JavaScript, so you cannot fix it from the browser alone. Use APIs that permit cross-origin requests, or route the call through your own backend server.