Learn
🤖 AI Hub 📰 News 📖 Dictionary 🧰 Resources
Grow
💼 Jobs 🚀 Community 🏆 Challenges
Explore by Stage
🌑 Zero 📚 Learn 🔧 Practice 🏗 Build 🚀 Launch 💰 Earn
Account
Login Sign Up
🏗️HTML 🎨CSS ⚡JavaScript 🐙Git & GitHub ⚛️React Soon 📘TypeScript Soon 🐍Python 🗄️Database & SQL Soon 🌐Web Development Soon ☁️Cloud & DevOps Soon 🤖AI & ML Soon 📄Resume Prep Soon 🎤Interview Prep Soon 💼Freelancing Soon 🚀SaaS & Side Projects Soon 🤖Automation Soon
Advanced 🕑 14 min read 👁 2 views 📅 Oct 21, 2026

Fetch API: Getting Live Data from the Internet

Use fetch to show live weather for Indian cities and load posts from a practice API. Handle status codes, loading and error states, query parameters, POST requests and timeouts.

Live Data on Your Page

Open any weather app and it shows the temperature in your city right now. Open a news site and the latest stories appear without anyone editing the page by hand. This data is not written into the HTML. The page asks a server for it while it runs, and shows whatever comes back.

In JavaScript, the tool for asking is fetch(). In this lesson you will show the current weather for New Delhi, Mumbai, Bengaluru, Chennai or Kolkata, and load a list of posts from a free practice service. Along the way you will learn how to handle slow networks, errors and sending your own data.

You need an internet connection for every example in this lesson. If you are offline, the examples show their error messages instead, which is also a good test of your error handling.

This lesson uses async and await from the JavaScript async and await lesson, and JSON from the JavaScript JSON lesson.

What Is an API?

An API (Application Programming Interface) is a set of addresses on a server that programs can call to get or send data. Think of a railway enquiry counter. You do not walk into the control room. You ask the counter a question in a fixed format, “status of train number so and so”, and you get a short answer. A web API works the same way: you request a URL, and the server replies with data, usually in JSON format.

We will use two free APIs that need no account or key:

APIWhat it gives
jsonplaceholder.typicode.comFake posts, users and comments for practice. It accepts POST requests but does not really save them.
api.open-meteo.comReal weather data for any latitude and longitude.

Your First GET Request

A GET request asks for data. It is what fetch(url) does by default. fetch returns a promise of a Response object. The response arrives in two parts: first the status and headers, then the body. That is why you need two awaits: one for fetch and one for response.json(), which reads the body and parses it from JSON into a JavaScript object.

// Needs an internet connection
async function getFirstPost() {
  const response = await fetch("https://jsonplaceholder.typicode.com/posts/1");
  console.log(response.status);   // 200
  console.log(response.ok);       // true

  const post = await response.json();   // read the body and parse the JSON
  console.log(post.id);           // 1
  console.log(post.title);        // sunt aut facere repellat provident occaecati excepturi optio reprehenderit
}

getFirstPost();

response.ok and Status Codes

Every HTTP response has a status code, a number that says how the request went:

CodeMeaning
200OK, here is your data
201Created, your new item was saved
400Bad request, something in your request is wrong
404Not found, nothing exists at that address
500Server error, the problem is on the server

Here is the surprise: fetch does not reject on 404 or 500. It only rejects when the request could not happen at all, for example when you are offline. A 404 is still a successful conversation with the server, just with bad news. So you must check response.ok, which is true for codes 200 to 299, and throw your own error when it is false. A small helper like getJson saves you from repeating this check:

// Needs an internet connection
async function getJson(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error("Request failed with status " + response.status);
  }
  return response.json();
}

async function main() {
  try {
    const post = await getJson("https://jsonplaceholder.typicode.com/posts/2");
    console.log("Found:", post.title);   // Found: qui est esse

    await getJson("https://jsonplaceholder.typicode.com/posts/99999");
    console.log("This line never runs");
  } catch (error) {
    console.log(error.message);          // Request failed with status 404
  }
}

main();

Query Parameters with URLSearchParams

Many APIs read options from the query string, the part of the URL after ?, written as key=value pairs joined by &. You could build it by joining strings, but special characters like spaces, commas and & must be encoded. URLSearchParams does the encoding for you. Here it builds the weather URL for New Delhi:

// Needs an internet connection
async function getWeather(latitude, longitude) {
  const params = new URLSearchParams({
    latitude: latitude,
    longitude: longitude,
    current: "temperature_2m,wind_speed_10m",
  });
  const url = "https://api.open-meteo.com/v1/forecast?" + params.toString();
  console.log(url);
  // https://api.open-meteo.com/v1/forecast?latitude=28.61&longitude=77.21&current=temperature_2m%2Cwind_speed_10m

  const response = await fetch(url);
  if (!response.ok) {
    throw new Error("Weather service error: " + response.status);
  }
  const data = await response.json();
  return data.current;
}

getWeather(28.61, 77.21)
  .then((now) => {
    console.log("New Delhi temperature:", now.temperature_2m, "C");   // for example 31.4 C
    console.log("Wind speed:", now.wind_speed_10m, "km/h");          // for example 9.2 km/h
  })
  .catch((error) => console.log(error.message));

The comma became %2C, its encoded form, and the server understands it. To show another city, change the latitude and longitude: Mumbai is 19.08, 72.88, Bengaluru is 12.97, 77.59, Chennai is 13.08, 80.27 and Kolkata is 22.57, 88.36. Your temperature will be different, because the data is live.

Loading and Error States on the Page

A network request can take a second or two on a slow mobile connection, and sometimes it fails. A good page always shows one of three states: loading, success or error. Never leave the user staring at an empty box. This page loads five posts. The _limit parameter is a feature of this practice API:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Latest Posts</title>
  <style>
    body { font-family: sans-serif; padding: 16px; }
    .post { border-bottom: 1px solid #ddd; padding: 8px 0; }
    .post h3 { margin: 0 0 4px; font-size: 16px; text-transform: capitalize; }
    .error { color: crimson; }
  </style>
</head>
<body>
  <h2>Latest Posts</h2>
  <p id="status">Loading posts...</p>
  <div id="posts"></div>

  <script>
    // Needs an internet connection
    const statusText = document.querySelector("#status");
    const list = document.querySelector("#posts");

    async function loadPosts() {
      try {
        const params = new URLSearchParams({ _limit: 5 });
        const response = await fetch("https://jsonplaceholder.typicode.com/posts?" + params);
        if (!response.ok) {
          throw new Error("Server replied with status " + response.status);
        }
        const posts = await response.json();

        posts.forEach((post) => {
          const div = document.createElement("div");
          div.className = "post";
          const title = document.createElement("h3");
          title.textContent = post.id + ". " + post.title;
          const body = document.createElement("p");
          body.textContent = post.body;
          div.append(title, body);
          list.append(div);
        });
        statusText.textContent = "Showing " + posts.length + " posts";
      } catch (error) {
        statusText.className = "error";
        statusText.textContent = "Could not load posts. Check your internet connection. (" + error.message + ")";
      }
    }

    loadPosts();
  </script>
</body>
</html>

Notice the code uses textContent to put the data on the page, not innerHTML. Data from a server should be treated as plain text, so that any HTML hidden inside it cannot run on your page.

Sending JSON with POST

A POST request sends data to the server, for example a new post, a form or an order. You pass a second argument to fetch with three things:

  • method: "POST" to say what kind of request this is.
  • headers: extra information about the request. Content-Type: application/json tells the server the body is JSON.
  • body: the data itself, turned into a JSON string with JSON.stringify().
// Needs an internet connection
async function createPost() {
  const newPost = {
    title: "College fest registrations are open",
    body: "Register for coding, quiz and dance events before Friday.",
    userId: 1,
  };

  const response = await fetch("https://jsonplaceholder.typicode.com/posts", {
    method: "POST",
    headers: {
      "Content-Type": "application/json; charset=UTF-8",
    },
    body: JSON.stringify(newPost),
  });

  console.log(response.status);   // 201  (Created)
  const saved = await response.json();
  console.log(saved.id);          // 101  (a fake id from the practice API)
  console.log(saved.title);       // College fest registrations are open
}

createPost().catch((error) => console.log("Failed:", error.message));

The practice API replies with status 201 and your post plus a new id of 101. It does not really save anything, so fetching post 101 later gives a 404. A real API would store it.

Timeouts with AbortController

By default, a fetch can wait a very long time on a bad network. An AbortController lets you cancel a request. You pass its signal to fetch, and call controller.abort() when you want to give up. The fetch promise then rejects with an error named AbortError.

// Needs an internet connection
async function fetchWithTimeout(url, ms) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), ms);

  try {
    const response = await fetch(url, { signal: controller.signal });
    return await response.json();
  } finally {
    clearTimeout(timer);
  }
}

async function main() {
  try {
    const users = await fetchWithTimeout("https://jsonplaceholder.typicode.com/users", 5000);
    console.log("Loaded", users.length, "users");   // Loaded 10 users

    await fetchWithTimeout("https://jsonplaceholder.typicode.com/photos", 1);
  } catch (error) {
    console.log(error.name);   // AbortError  (1 ms was far too short)
  }
}

main();

Modern browsers also offer a shortcut, fetch(url, { signal: AbortSignal.timeout(5000) }), which does the same thing in one line. Aborting is also useful in search boxes, to cancel an old request when the user types again.

A Word About CORS

Browsers protect users with a rule called the same-origin policy: a page from one website cannot read responses from another website unless that other website allows it. The server gives permission by sending special headers, a system called CORS (Cross-Origin Resource Sharing). Both APIs in this lesson send these headers, so they work from any page. If you see a console error mentioning CORS with some other API, your code is usually fine: that server simply does not allow browsers on other sites to read it, and you would need to call it from your own server instead.

Real-Life Example: A City Weather Card

Here is a small weather app for five Indian cities. Choosing a city or pressing Refresh shows a loading message, fetches live data, and fills a card. If anything goes wrong, the card shows a clear error. The units, such as °C and km/h, come from the API response itself.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>City Weather</title>
  <style>
    body { font-family: sans-serif; padding: 16px; max-width: 360px; }
    select, button { padding: 8px; font-size: 16px; }
    .card { margin-top: 16px; padding: 16px; border-radius: 12px; background: #eef6ff; }
    .temp { font-size: 40px; font-weight: bold; margin: 4px 0; }
    .error { color: crimson; }
  </style>
</head>
<body>
  <h2>Weather Right Now</h2>
  <select id="city">
    <option value="28.61,77.21">New Delhi</option>
    <option value="19.08,72.88">Mumbai</option>
    <option value="12.97,77.59">Bengaluru</option>
    <option value="13.08,80.27">Chennai</option>
    <option value="22.57,88.36">Kolkata</option>
  </select>
  <button id="refresh">Refresh</button>
  <div class="card" id="card">Choose a city.</div>

  <script>
    // Needs an internet connection
    const select = document.querySelector("#city");
    const card = document.querySelector("#card");
    const button = document.querySelector("#refresh");

    async function showWeather() {
      const [latitude, longitude] = select.value.split(",");
      const cityName = select.options[select.selectedIndex].text;
      card.textContent = "Loading weather for " + cityName + "...";
      button.disabled = true;

      try {
        const params = new URLSearchParams({
          latitude: latitude,
          longitude: longitude,
          current: "temperature_2m,wind_speed_10m",
        });
        const response = await fetch("https://api.open-meteo.com/v1/forecast?" + params);
        if (!response.ok) {
          throw new Error("status " + response.status);
        }
        const data = await response.json();
        const now = data.current;
        const units = data.current_units;

        card.innerHTML = "";
        const heading = document.createElement("strong");
        heading.textContent = cityName;
        const temp = document.createElement("p");
        temp.className = "temp";
        temp.textContent = now.temperature_2m + " " + units.temperature_2m;
        const wind = document.createElement("p");
        wind.textContent = "Wind: " + now.wind_speed_10m + " " + units.wind_speed_10m;
        card.append(heading, temp, wind);
      } catch (error) {
        card.innerHTML = "";
        const message = document.createElement("p");
        message.className = "error";
        message.textContent = "Could not load the weather (" + error.message + "). Are you online?";
        card.append(message);
      } finally {
        button.disabled = false;
      }
    }

    select.addEventListener("change", showWeather);
    button.addEventListener("click", showWeather);
    showWeather();
  </script>
</body>
</html>

Try these changes:

  1. Add your own city to the list. Search the web for its latitude and longitude.
  2. Add relative_humidity_2m to the current parameter (separated by a comma) and show the humidity on the card.
  3. Turn off your Wi-Fi or mobile data, press Refresh and check that the error message appears.

Common Mistakes

  • Forgetting to await response.json(). response.json() returns a promise. Without await, you get a pending promise instead of your data.
  • Thinking fetch fails on 404. It does not. Always check response.ok and throw your own error.
  • Sending an object without JSON.stringify. Passing body: newPost sends the text [object Object]. Convert it to a JSON string and set the Content-Type header.
  • No loading or error state. On a slow connection, an empty page looks broken. Show “Loading...” first and a friendly message in catch.
  • Putting server data into innerHTML. Use textContent for text that comes from an API, to keep the page safe.

Practice Task: Load Comments for a Post

The practice API has comments at https://jsonplaceholder.typicode.com/comments, and you can filter them with ?postId=1. Build a small page where the user types a post number and loads its comments. Your page should:

  • Show “Loading...” while waiting.
  • Build the URL with URLSearchParams.
  • Check response.ok and throw an error when it is false.
  • List each comment’s name and show how many comments were found.
  • Show a friendly message if anything fails.
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Post Comments</title>
</head>
<body>
  <h2>Comments</h2>
  <input id="postId" type="number" min="1" max="100" value="1">
  <button id="load">Load comments</button>
  <p id="status"></p>
  <ul id="comments"></ul>

  <script>
    // Needs an internet connection
    // TODO 1: when the button is clicked, show "Loading..." in #status
    // TODO 2: fetch https://jsonplaceholder.typicode.com/comments with
    //         URLSearchParams so the URL ends with ?postId=<the number typed>
    // TODO 3: if response.ok is false, throw an Error with the status
    // TODO 4: show each comment's name in an <li>, and "5 comments" in #status
    // TODO 5: on any error, show a friendly message in #status
  </script>
</body>
</html>
Show one possible answer (try it yourself first!)
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Post Comments</title>
</head>
<body>
  <h2>Comments</h2>
  <input id="postId" type="number" min="1" max="100" value="1">
  <button id="load">Load comments</button>
  <p id="status"></p>
  <ul id="comments"></ul>

  <script>
    // Needs an internet connection
    const statusText = document.querySelector("#status");
    const list = document.querySelector("#comments");

    document.querySelector("#load").addEventListener("click", async () => {
      const postId = document.querySelector("#postId").value;
      statusText.textContent = "Loading...";
      list.innerHTML = "";

      try {
        const params = new URLSearchParams({ postId: postId });
        const response = await fetch("https://jsonplaceholder.typicode.com/comments?" + params);
        if (!response.ok) {
          throw new Error("status " + response.status);
        }
        const comments = await response.json();

        comments.forEach((comment) => {
          const li = document.createElement("li");
          li.textContent = comment.name;
          list.append(li);
        });
        statusText.textContent = comments.length + " comments";
      } catch (error) {
        statusText.textContent = "Sorry, the comments could not be loaded (" + error.message + ").";
      }
    });
  </script>
</body>
</html>

Key Takeaways

  • fetch(url) returns a promise of a Response. Use await response.json() to get the data.
  • fetch rejects only on network failure. Check response.ok and the status code yourself.
  • Build query strings with URLSearchParams so values are encoded correctly.
  • To send data, use method: "POST", a Content-Type header and JSON.stringify for the body.
  • Always show loading and error states, and use AbortController to stop requests that take too long.
  • CORS is the server’s permission for other websites to read its data. Without it, the browser blocks the response.

❓ Frequently Asked Questions

The first await waits for the server to start replying with a status and headers. The second, on response.json(), waits for the whole body to download and be parsed. Large responses can take a while to finish arriving.
No. JSONPlaceholder and Open-Meteo are free services that work without a key, which makes them good for practice. Many other APIs need a key, which you should never put in public front-end code.
For new code, yes. fetch is the modern, promise-based way to make requests and works in all current browsers. XMLHttpRequest is older and still appears in legacy code.
fetch needs a network connection to reach the server. When you are offline, the promise rejects with a TypeError, which is exactly the case your catch block should handle.
Was this lesson helpful?

🎯 Finished this lesson?

🔗 Related Tutorials
🤖 Got a Doubt? Ask AI
Get instant answers in Hindi or English
🤖
Z2T Bot
Online
Ask me anything about this tutorial — I'm here! 👋