Promises + Fetch: Load Data from the Web
Introduction
Icebreaker: Have you ever waited for something that wasn't ready yet? A response to a text. An email. Approval from your boss. While you wait, you don't stop your life—you keep moving. Then when it arrives, you handle it.
That's how Promises work in JavaScript.
Scenario: So far, most of your data has been hardcoded in your JavaScript files. Real apps load data from servers. This lesson teaches the minimum Promise + Fetch workflow you need to do that.
Learning Objectives
By the end of this lesson, you will be able to:
- Explain what "asynchronous" means in plain language
- Use
fetch()with.then()and.catch() - Parse JSON with
response.json() - Log and inspect fetched data in the console
- Handle errors gracefully
- Explain JSON as a portable data format
Core Concept Overview
1. Asynchronous Means “Not Ready Yet”
Some work in JavaScript finishes instantly. Other work takes time—like asking a server for data over the internet.
JavaScript doesn't wait. It keeps running and comes back later when the data arrives.
That's asynchronous.
2. What Is fetch?
fetch() does exactly what the name says: it fetches data from a remote source.
fetch("https://example.com/data");
Give it a URL, and it starts the request.
But it does not hand you the data right away. It hands you a Promise.
3. What Is a Promise?
Think restaurant buzzer:
- You request a table.
- Host gives you a buzzer.
- While you wait, the buzzer is pending.
- When it buzzes, you check in and get your table.
fetch() works the same way:
fetch(...)gives you the buzzer (the Promise).then(...)is what you do when it buzzes (Promise fulfilled).catch(...)is what you do if something goes wrong
A Promise has three states:
- pending = waiting for the server
- fulfilled = data arrived successfully
- rejected = something went wrong (error)
Restaurant mapping:
- Buzzer in your hand, waiting → pending
- Buzzer goes off, you get seated → fulfilled
- Kitchen fire, no table available → rejected
So yes: fetch gets you the Promise first.
Then you process it with .then() (and .catch() for errors) to reach the actual data.
then() is you walking back to the host when the buzzer goes off so you can get the table (the data).
And catch is you realizing the restaurant is on fire and calling 991 instead - or similar.
4. JSON Is Portable Data
Servers send data as JSON—plain text that looks like a JavaScript object.
// JavaScript object
{ name: "Ava", age: 22 }
// JSON (same thing, but as text)
"{\"name\":\"Ava\",\"age\":22}"
response.json() converts JSON text into a JavaScript object you can use.
Think of response.json() as converting raw response text into data you can actually use in JavaScript.
Servers are remote computers that receive requests, do work (talk to databases, etc.), and send back responses. This is covered in How Computers and the Web Work.
Hands-On Application
Note: Use the assigned template repo to follow along with the Try Its. Each Try It builds on the previous one, so do them in order. Commit early, commit often!
In these examples, we are using DummyJSON, a free API for testing and prototyping. It provides endpoints like /todos that return JSON data we can practice with.
What's REST? For now: it's a common way APIs expose data over URLs, and fetch is how you request that data.
Try It 1: Just fetch() and Log It
In src/index.js, write:
const result = fetch("https://dummyjson.com/todos/1");
console.log(result);
Run it:
bun src/index.js
What do you see?
You see: Promise { <pending> }
That's not data. That's a Promise. That's your buzzer - not your table.
Try It 2: See What Happens When It Fulfills
Now use .then() to see what arrives when the Promise fulfills:
fetch("https://dummyjson.com/todos/1")
.then((response) => {
console.log("Promise fulfilled!");
console.log(response);
});
Run it. You see a response object. Remember the restaurant buzzer? The buzzer just went off. The Promise is fulfilled.
Try It 3: Extract the Data
The response object contains the data, but it's still wrapped as text (JSON).
Use response.json() to unwrap it:
fetch("https://dummyjson.com/todos/1")
.then((response) => {
return response.json();
})
.then((data) => {
console.log("Here's the actual data:");
console.log(data);
});
Run it. Now you see a real JavaScript object: { id: 1, todo: "...", completed: false }.
Notice the two .then()s:
- First gets the response
- Second gets the parsed data
Try It 4: Handle Errors with .catch()
Now add .catch() so your code can handle problems gracefully:
fetch("https://dummyjson.com/todos/1")
.then((response) => response.json())
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error("Something went wrong:", error.message);
});
Try breaking it with a bad URL (for example, change todos/1 to todoz/1) so you can see .catch() run.
Try It 5: Check response.ok
Now check HTTP status manually with response.ok.
fetch("https://dummyjson.com/todos/1")
.then((response) => {
if (!response.ok) {
console.log(`Server returned status: ${response.status}`);
return null;
}
return response.json();
})
.then((data) => {
if (data === null) return;
console.log(data);
})
.catch((error) => {
console.error("Something went wrong:", error.message);
});
This pattern becomes important when APIs return a response with a failed status code. For instance, if a database lookup fails, the server might return a 404 or 500 status. fetch won't reject the Promise in that case, so you have to check response.ok to handle it.
Try It 6: Load Multiple Todos
Now fetch a list instead of one todo, then do two useful data steps (filter and map):
fetch("https://dummyjson.com/todos")
.then((response) => response.json())
.then((data) => {
console.log("All todos:", data.todos);
console.log("Count:", data.todos.length);
const openTodos = data.todos.filter((item) => item.completed === false);
console.log(`Open todos: ${openTodos.length}`);
console.table(openTodos);
const titles = data.todos.map((todo) => todo.todo);
console.log("Todo titles:", titles);
})
.catch((error) => {
console.error("Failed:", error.message);
});
Notice: data.todos (not just data). The API wraps the array in an object. Always inspect the response shape first.
This is the complete Promise + Fetch pattern for this lesson.
What About the Browser? This lesson teaches fetching and inspecting data. In a later lesson, you'll learn how to render fetched data into the browser DOM. For now, mastering the data fetch itself is the goal.
Common Mistakes
Mistake 1: Forgetting return response.json()
// ❌ Wrong: next then gets undefined
.then((response) => {
response.json();
})
// ✅ Right
.then((response) => {
return response.json();
})
Mistake 2: Assuming fetch rejects on 404
fetch only rejects for network-level failures (no internet, DNS timeout, etc.).
HTTP status errors like 404 or 500 still return a response object.
That is why we check response.ok manually.
// ❌ This won't catch a 404
fetch("https://api.example.com/missing")
.then((response) => response.json())
.catch(console.error); // Won't run if status is 404
// ✅ This handles failed status codes
fetch("https://api.example.com/missing")
.then((response) => {
if (!response.ok) {
console.log(`Status ${response.status}`);
return null;
}
return response.json();
})
.then((data) => {
if (data === null) return;
console.log(data);
})
.catch(console.error); // Runs for 404, 500, etc.
Mistake 3: Forgetting .catch()
If you don't handle errors, fetch failures fail silently and confuse you.
Always add .catch().
Mistake 4: Not Understanding Response Shape
Different APIs return different structures. Always console.log() the full response first.
// ❌ Wrong if API returns {todos: [...]} not just [...]
data.map(...)
// ✅ Right
data.todos.map(...)
Wrap-Up & Assessment
Key Takeaways
fetch()returns a Promise.then()handles success,.catch()handles errorsresponse.json()converts response text into JavaScript data- Always inspect the API response shape before using it
console.log()andconsole.table()are your debugging friends
Reflection Questions
- In Try It 1, why did we see
Promise { <pending> }instead of the data? - What does "fulfilled" mean for a Promise? What does "rejected" mean?
- Why do we need TWO
.then()calls in Try It 3? - How did you trigger
.catch()in Try It 4? - Write one sentence: When would you actually use
.catch()?
Submission Checklist
- Completed Try Its 1–6
- Screenshot of Try It 1 output (showing the Promise object)
- Screenshot of Try It 6 output (showing total loaded todos)
- Screenshot of Try It 6 output (showing filtered open todos)
- Screenshot of error handling (modify a URL to break it)
- At least 3 commits (one per few Try Its)
- Reflection questions answered
Looking Ahead
Next, you'll use fetch inside form submissions and event listeners to build interactive apps that load data when users click buttons or submit forms.