fetch — Talking to Your API
Intermediatefetch is the built-in HTTP client. Its one surprising behaviour is that it does not reject on 404 or 500 — you have to check response.ok yourself.
Overview
Every frontend that talks to a backend does it through fetch, directly or through a wrapper. The API is small: a URL, an options object, and a response whose body you read with .json() or .text(). The trap that catches everyone once is that a rejected promise means the request never completed — a network failure or CORS block — while an HTTP error status is a perfectly successful fetch that happens to carry a 500. If you do not check response.ok, your error path never runs and you try to parse an error page as JSON.
The Shape of a Request
GET is the default. Anything else needs a method, usually a Content-Type, and a stringified body.
// GET
const res = await fetch('/api/problems?page=1')
// POST with JSON
const res = await fetch('/api/problems', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ title, difficulty }),
})
// File upload — do NOT set Content-Type; the browser adds the boundary
const form = new FormData()
form.append('file', fileInput.files[0])
await fetch('/api/upload', { method: 'POST', body: form })
// Cookies are not sent cross-origin unless you ask
await fetch(url, { credentials: 'include' })Error Handling That Actually Works
The rule: reject means the request failed to happen; !res.ok means it happened and the server said no.
async function api(path, options) {
let res
try {
res = await fetch(path, options)
} catch (err) {
// Network down, DNS failure, CORS block — never reached the server
throw new Error('Network error — check your connection')
}
if (!res.ok) {
// The request succeeded; the server returned 4xx/5xx
const body = await res.json().catch(() => ({}))
throw new Error(body.detail ?? `HTTP ${res.status}`)
}
return res.status === 204 ? null : res.json()
}
// Without the !res.ok check, a 500 returning an HTML error page
// reaches res.json() and throws "Unexpected token <" instead.Cancelling and Timing Out
AbortController cancels a request. In React this is what stops a stale response overwriting a newer one.
const controller = new AbortController()
fetch(url, { signal: controller.signal })
controller.abort() // rejects with an AbortError
// Timeout, built in:
await fetch(url, { signal: AbortSignal.timeout(5000) })
// The React pattern — cancel on unmount or when the query changes
useEffect(() => {
const c = new AbortController()
fetch(`/api/search?q=${q}`, { signal: c.signal })
.then(r => r.json())
.then(setResults)
.catch(err => { if (err.name !== 'AbortError') setError(err) })
return () => c.abort()
}, [q])Key Points to Remember
- 1fetch rejects only on network failure — a 404 or 500 is a successful fetch, so always check response.ok
- 2Without the ok check, an HTML error page reaches res.json() and throws a confusing parse error
- 3Do not set Content-Type when sending FormData — the browser must add the multipart boundary
- 4Cross-origin cookies are not sent unless you pass credentials: "include"
- 5AbortController cancels in-flight requests; aborting on cleanup is what stops a stale response overwriting a newer one
Interview Questions
Sign in to ask AriaDoes fetch throw an error for a 404 response? What do you have to check?
How do you add a timeout to a fetch request?
Why should you not set the Content-Type header when uploading FormData?
Ask Aria about fetch — Talking to Your API
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.