Asynchronous JS, APIs, and JSON
Fetch and process data asynchronously, design and consume APIs, and add realtime features.
Content
Fetch API fundamentals
Versions:
Watch & Learn
AI-discovered learning video
Sign in to watch the learning video for this topic.
Fetch API Fundamentals — Asynchronous JS, APIs, and JSON
'This is the moment where the concept finally clicks.'
You already know how to use modern JavaScript to manipulate the DOM, write template literals, destructure objects, and even wrap things in classes. Now we step into the place where the browser talks to the internet: the Fetch API. Think of fetch as the browser's polite messenger: it goes out, asks a server for something, and brings the answer back in a Promise-wrapped parcel.
Why this matters
- APIs are everywhere: user profiles, weather, stock quotes, or that cat image endpoint you use to procrastinate.
- JSON is the lingua franca for data on the web.
- The Fetch API is the standard way in modern browsers to request that data asynchronously.
If you've used template literals to build URLs or destructuring to pick values out of objects, you're already halfway to being fluent in async data fetching.
Core idea: fetch returns a Promise
The basic shape looks like this:
fetch(url)
.then(response => response.json())
.then(data => console.log(data))
.catch(err => console.error('Network or parsing error', err))
Key points:
- fetch returns a Promise that resolves to a Response object when the request completes at the network level.
- response.json() also returns a Promise that resolves to the parsed JavaScript object.
- A resolved fetch does not mean the HTTP status is 200. It only means the network request completed.
Common mistake
Many students assume fetch rejects on HTTP errors (like 404). It does not. You need to check response.ok or response.status yourself.
const res = await fetch(url)
if (!res.ok) throw new Error(`HTTP error ${res.status}`)
const data = await res.json()
Async/await vs .then chaining
You already use try/catch from the previous module. That plays nicely with async/await.
Promise chain style:
fetch(url)
.then(res => {
if (!res.ok) throw new Error('Bad response')
return res.json()
})
.then(data => render(data))
.catch(err => showError(err))
Async/await style (cleaner for linear logic):
async function loadAndRender() {
try {
const res = await fetch(url)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
render(data)
} catch (err) {
showError(err)
}
}
Use async/await for readability; use .then when composing many independent promises.
Making requests: GET, POST, headers, body
Fetch defaults to GET. To POST JSON:
const payload = { name: 'Ada', project: 'CS50' }
const res = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
const result = await res.json()
Notes:
- Always set Content-Type when sending JSON.
- Use JSON.stringify on the JS object.
- Check response.ok before relying on result.
Parsing JSON and destructuring
You learned destructuring earlier. Use it to make your life easier when extracting fields from API responses.
const res = await fetch('/api/profile')
if (!res.ok) throw new Error('Profile not found')
const profile = await res.json()
// destructuring
const { id, username, avatarUrl } = profile
Perfect for when you just need a few fields to render a component.
Organizing fetch logic with classes (remember inheritance?)
Since you know classes and inheritance, consider encapsulating API interactions inside a class. This makes tests, reuse, and mocking easier.
class ApiClient {
constructor(baseUrl) {
this.baseUrl = baseUrl
}
async get(path) {
const res = await fetch(`${this.baseUrl}${path}`)
if (!res.ok) throw new Error(`GET ${path} failed: ${res.status}`)
return res.json()
}
async post(path, body) {
const res = await fetch(`${this.baseUrl}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
if (!res.ok) throw new Error(`POST ${path} failed: ${res.status}`)
return res.json()
}
}
class AuthApiClient extends ApiClient {
constructor(baseUrl, token) {
super(baseUrl)
this.token = token
}
async get(path) {
const res = await fetch(`${this.baseUrl}${path}`, {
headers: { Authorization: `Bearer ${this.token}` }
})
if (!res.ok) throw new Error(`Auth GET ${path} failed: ${res.status}`)
return res.json()
}
}
This pattern uses inheritance to add authentication behavior. Neat, modular, testable.
AbortController: cancelling requests
Sometimes the user navigates away or types a new search while the previous request is still in flight. Cancel it.
const controller = new AbortController()
fetch('/api/search?q=hello', { signal: controller.signal })
.then(res => res.json())
.then(render)
.catch(err => {
if (err.name === 'AbortError') return
console.error(err)
})
// later
controller.abort() // cancels the request
Pitfalls and practical tips
- Network vs HTTP errors: fetch rejects for network errors; it resolves for HTTP errors. Check response.ok.
- Always await response.json() inside try/catch to handle invalid JSON.
- CORS: if you get blocked, the server must set appropriate CORS headers. This is a server-side setting, not a client bug.
- Credentials: to send cookies, include credentials: 'include' in options.
- Rate limits and retries: consider exponential backoff for transient failures.
Quick example: search widget using template literals and DOM
const form = document.querySelector('#searchForm')
form.addEventListener('submit', async e => {
e.preventDefault()
const q = new FormData(form).get('q')
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`)
if (!res.ok) throw new Error('Search failed')
const { results } = await res.json() // destructure
renderResults(results)
} catch (err) {
showError(err)
}
})
This ties your previous knowledge together: template literals for the URL, destructuring for response fields, try/catch for errors, and DOM updates to display results.
Key takeaways
- fetch is Promise-based. Check response.ok and parse JSON with response.json().
- Use async/await plus try/catch for readable flow and robust error handling.
- Use classes to encapsulate API logic and inheritance to extend behavior.
- Remember CORS, content-type headers, and aborting long requests.
Final thought
'Fetch is less about magic and more about disciplined plumbing: check the status, parse the payload, and handle the chaos.'
You now have the fundamentals to call APIs, parse JSON, and integrate remote data into your interactive pages. Next up: handling streaming responses, websockets, and more advanced API patterns — or just building the coolest cat gallery the internet has ever seen.
Comments (0)
Please sign in to leave a comment.
No comments yet. Be the first to comment!