Skip to main content

Working with Data

Real web applications need data - from APIs, from the user, from the browser. This chapter covers fetching data from external sources, handling JSON, and storing data locally.

The Fetch API​

fetch is the built-in way to make HTTP requests in JavaScript:

fetch("https://jsonplaceholder.typicode.com/users/1")
.then((response) => response.json())
.then((data) => console.log(data.name))
.catch((error) => console.error("Error:", error));

Result:

Leanne Graham

This uses promises - fetch returns a promise that resolves to a Response object. .json() parses the response body as JSON (also returns a promise).

Promises - a quick overview​

A promise represents a value that will be available in the future. It has three states:

StateMeaning
PendingOperation in progress
FulfilledOperation succeeded, value available
RejectedOperation failed, error available
const promise = fetch("https://jsonplaceholder.typicode.com/users/1");

promise
.then((response) => {
console.log("Status:", response.status);
return response.json();
})
.then((user) => {
console.log("User:", user.name);
})
.catch((error) => {
console.error("Failed:", error.message);
})
.finally(() => {
console.log("Request complete");
});

Result:

Status: 200
User: Leanne Graham
Request complete
  • .then() runs when the promise fulfills
  • .catch() runs when the promise rejects
  • .finally() runs either way

For a deep dive into async patterns, see Async/Await Guide.

async / await​

A cleaner syntax for working with promises:

async function getUser(id) {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
const user = await response.json();
return user;
}

async function main() {
const user = await getUser(1);
console.log(user.name);
console.log(user.email);
}

main();

Result:

Leanne Graham
Sincere@april.biz

await pauses execution until the promise resolves. It can only be used inside an async function.

Error handling with fetch​

fetch rejects for network-level failures (no internet, DNS failure), aborted requests, or blocked requests (for example, CORS). HTTP errors like 404 or 500 are not rejections - you must check response.ok:

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

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

return await response.json();
}

async function main() {
try {
const user = await getUser(1);
console.log("User:", user.name);
} catch (error) {
console.error("Failed to fetch user:", error.message);
}
}

main();

Result:

User: Leanne Graham

With an invalid ID (the API returns 404):

try {
const user = await getUser(9999);
} catch (error) {
console.error(error.message);
}

Result:

HTTP error: 404 Not Found

A reusable fetch helper​

async function fetchJSON(url) {
const response = await fetch(url);

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

return await response.json();
}

// Usage
async function main() {
try {
const users = await fetchJSON("https://jsonplaceholder.typicode.com/users");
console.log(`Fetched ${users.length} users`);

const posts = await fetchJSON("https://jsonplaceholder.typicode.com/posts?userId=1");
console.log(`User 1 has ${posts.length} posts`);
} catch (error) {
console.error("API error:", error.message);
}
}

main();

Result:

Fetched 10 users
User 1 has 10 posts

Sending data with fetch​

POST request​

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

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

return await response.json();
}

async function main() {
const post = await createPost("My Title", "My post content");
console.log("Created post:", post);
}

main();

Result:

Created post: { title: 'My Title', body: 'My post content', userId: 1, id: 101 }

Other HTTP methods​

// PUT - replace a resource
await fetch("https://api.example.com/users/1", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Updated Name" }),
});

// PATCH - partially update a resource
await fetch("https://api.example.com/users/1", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Updated Name" }),
});

// DELETE - remove a resource
await fetch("https://api.example.com/users/1", {
method: "DELETE",
});

Query parameters​

Build URLs safely using URLSearchParams:

const params = new URLSearchParams({
q: "javascript",
page: "1",
limit: "10",
});

const url = `https://api.example.com/search?${params}`;
console.log(url);

Result:

https://api.example.com/search?q=javascript&page=1&limit=10

URLSearchParams handles special characters automatically:

const params = new URLSearchParams({ q: "hello world & more" });
console.log(params.toString());

Result:

q=hello+world+%26+more

Parallel requests​

When fetching independent data, run requests in parallel:

async function getDashboardData() {
// Sequential - slow (each waits for the previous)
// const users = await fetchJSON("/api/users");
// const posts = await fetchJSON("/api/posts");

// Parallel - fast (both run at the same time)
const [users, posts] = await Promise.all([
fetchJSON("https://jsonplaceholder.typicode.com/users"),
fetchJSON("https://jsonplaceholder.typicode.com/posts"),
]);

console.log(`${users.length} users, ${posts.length} posts`);
}

getDashboardData();

Result:

10 users, 100 posts

Promise.all runs all promises concurrently and waits for all to complete. If any fails, the entire Promise.all rejects.

Loading data into the DOM​

Combine fetch with DOM manipulation to build dynamic pages:

<div id="app">
<h1>Users</h1>
<div id="loading">Loading...</div>
<ul id="user-list"></ul>
<div id="error" style="display: none; color: red;"></div>
</div>
async function loadUsers() {
const loading = document.querySelector("#loading");
const list = document.querySelector("#user-list");
const errorDiv = document.querySelector("#error");

try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");

if (!response.ok) {
throw new Error(`Failed to load users: ${response.status}`);
}

const users = await response.json();

// Hide loading indicator
loading.style.display = "none";

// Build the list
for (const user of users) {
const li = document.createElement("li");

const name = document.createElement("strong");
name.textContent = user.name;

const email = document.createElement("span");
email.textContent = ` - ${user.email}`;

li.appendChild(name);
li.appendChild(email);
list.appendChild(li);
}
} catch (error) {
loading.style.display = "none";
errorDiv.style.display = "block";
errorDiv.textContent = error.message;
}
}

document.addEventListener("DOMContentLoaded", loadUsers);

This pattern - loading indicator, fetch data, build DOM, handle errors - is the foundation of every data-driven web page.

localStorage and sessionStorage​

The browser provides two storage mechanisms for saving data on the client:

FeaturelocalStoragesessionStorage
PersistsUntil manually clearedUntil the tab is closed
ScopeShared across tabs (same origin)Per tab
Size limit~5–10 MB~5–10 MB

Basic usage​

// Store a value
localStorage.setItem("username", "Ada");

// Read a value
const username = localStorage.getItem("username");
console.log(username);

// Remove a value
localStorage.removeItem("username");

// Clear everything
localStorage.clear();

Result:

Ada

Storing objects and arrays​

localStorage only stores strings. Use JSON.stringify and JSON.parse:

const settings = {
theme: "dark",
fontSize: 16,
language: "en",
};

// Save
localStorage.setItem("settings", JSON.stringify(settings));

// Load
const loaded = JSON.parse(localStorage.getItem("settings"));
console.log(loaded);
console.log(loaded.theme);

Result:

{ theme: 'dark', fontSize: 16, language: 'en' }
dark

Safe loading with fallback​

function loadFromStorage(key, fallback) {
const stored = localStorage.getItem(key);
if (stored === null) {
return fallback;
}
try {
return JSON.parse(stored);
} catch {
return fallback;
}
}

const theme = loadFromStorage("theme", "light");
console.log(theme);

Result (if nothing is stored):

light

Practical: persisting a to-do list​

const STORAGE_KEY = "todos";

function loadTodos() {
return loadFromStorage(STORAGE_KEY, []);
}

function saveTodos(todos) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
}

// Usage
let todos = loadTodos();
todos.push({ text: "Learn JavaScript", done: false });
todos.push({ text: "Build a website", done: false });
saveTodos(todos);

// Later, or on page reload
const restored = loadTodos();
console.log(restored);

Result:

[
{ text: 'Learn JavaScript', done: false },
{ text: 'Build a website', done: false }
]

sessionStorage​

Works identically to localStorage, but data is cleared when the tab closes:

sessionStorage.setItem("tempData", "gone when tab closes");
const temp = sessionStorage.getItem("tempData");
console.log(temp);

Result:

gone when tab closes

Use sessionStorage for data that should not persist (e.g., form state during a checkout flow).

Putting it all together: a data-driven page​

Here is a complete example that fetches user data, displays it, and caches results in localStorage:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User Directory</title>
<style>
body {
font-family: sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.user-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
}
.user-card h3 { margin: 0 0 8px; }
.user-card p { margin: 4px 0; color: #555; }
.loading { color: #888; font-style: italic; }
.error { color: red; }
button {
padding: 8px 16px;
margin-bottom: 16px;
cursor: pointer;
}
</style>
</head>
<body>
<h1>User Directory</h1>
<button id="refresh-btn">Refresh Data</button>
<div id="status"></div>
<div id="user-list"></div>

<script>
const CACHE_KEY = "cached_users";
const API_URL = "https://jsonplaceholder.typicode.com/users";

const statusDiv = document.querySelector("#status");
const listDiv = document.querySelector("#user-list");
const refreshBtn = document.querySelector("#refresh-btn");

function renderUsers(users) {
listDiv.innerHTML = "";
for (const user of users) {
const card = document.createElement("div");
card.classList.add("user-card");

const name = document.createElement("h3");
name.textContent = user.name;

const email = document.createElement("p");
email.textContent = user.email;

const city = document.createElement("p");
city.textContent = `City: ${user.address.city}`;

card.appendChild(name);
card.appendChild(email);
card.appendChild(city);
listDiv.appendChild(card);
}
}

async function fetchUsers(useCache = true) {
// Try cache first
if (useCache) {
const cached = localStorage.getItem(CACHE_KEY);
if (cached) {
try {
statusDiv.textContent = "Loaded from cache";
statusDiv.className = "";
renderUsers(JSON.parse(cached));
return;
} catch {
localStorage.removeItem(CACHE_KEY);
}
}
}

statusDiv.textContent = "Loading...";
statusDiv.className = "loading";

try {
const response = await fetch(API_URL);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}

const users = await response.json();

// Cache the result
localStorage.setItem(CACHE_KEY, JSON.stringify(users));

statusDiv.textContent = `Loaded ${users.length} users from API`;
statusDiv.className = "";
renderUsers(users);
} catch (error) {
statusDiv.textContent = `Error: ${error.message}`;
statusDiv.className = "error";
}
}

refreshBtn.addEventListener("click", () => {
localStorage.removeItem(CACHE_KEY);
fetchUsers(false);
});

document.addEventListener("DOMContentLoaded", () => fetchUsers());
</script>
</body>
</html>

This example demonstrates:

  • Fetching data from an API
  • Error handling with user-friendly messages
  • Rendering data into the DOM safely (no innerHTML with user data)
  • Caching with localStorage and a refresh button
  • Loading states

Summary​

  • fetch makes HTTP requests; always check response.ok for HTTP errors.
  • async/await makes promise-based code read like synchronous code.
  • Wrap fetch calls in try/catch for error handling.
  • Use Promise.all for parallel requests.
  • localStorage persists data across sessions; sessionStorage only until the tab closes.
  • Both storage APIs only store strings - use JSON.stringify/JSON.parse for objects.
  • Combine fetch, DOM manipulation, and storage for data-driven pages.

Next up: Project: Build a Complete Website - putting everything together into a real, multi-page site.