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:
| State | Meaning |
|---|---|
| Pending | Operation in progress |
| Fulfilled | Operation succeeded, value available |
| Rejected | Operation 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:
| Feature | localStorage | sessionStorage |
|---|---|---|
| Persists | Until manually cleared | Until the tab is closed |
| Scope | Shared 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
innerHTMLwith user data) - Caching with
localStorageand a refresh button - Loading states
Summary
fetchmakes HTTP requests; always checkresponse.okfor HTTP errors.async/awaitmakes promise-based code read like synchronous code.- Wrap
fetchcalls intry/catchfor error handling. - Use
Promise.allfor parallel requests. localStoragepersists data across sessions;sessionStorageonly until the tab closes.- Both storage APIs only store strings -- use
JSON.stringify/JSON.parsefor 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.