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 only rejects on network errors (no internet, DNS failure). 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) {
statusDiv.textContent = "Loaded from cache";
statusDiv.className = "";
renderUsers(JSON.parse(cached));
return;
}
}

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.