Fetching Data: Loading, Success and Failure
Fetching Data: Loading, Success and Failure
A school directory needs student names from a server. The request may be pending, fail, or return an unsuccessful HTTP status; the screen should explain each case rather than show an empty success panel.
Fetching retrieves data through a network API. In a client component, a request can be synchronized with the component’s selected resource, but loading, successful data and failure need explicit states and cleanup.
Understand
Represent request status clearly.
Build
Check HTTP success and parse the expected response.
Check
Distinguish local fixture data from a real service.
A request has more than one outcome
Pending
Display a loading message while no result exists.
Receive
Check the response and decode the expected data.
Settle
Show the directory or a recoverable failure message.
A response is not automatically successful data
Browser fetch returns a promise for a response. It rejects for failures such as some network problems, but HTTP errors such as 404 still produce responses. Check response.ok before reading the body as successful data. response.json is another asynchronous operation and can fail if the body is not valid JSON.
For this school contract, the JSON value is an array of student objects with numeric id and string name fields. Production code should validate that shape rather than trust an arbitrary payload. The browser demonstration uses a local data URL containing fixture JSON, so it contacts no school service and needs no unavailable backend.
Fetch an explicit local fixture
This is actual browser fetch, with a data URL containing two students. It demonstrates asynchronous loading and JSON parsing without pretending that a remote school API exists. The initial screen says Loading students; after completion it lists Maya and Ravi.
React · JavaScript with JSX
import { useEffect, useState } from 'react';
const fixture = 'data:application/json,' + encodeURIComponent(JSON.stringify([{ id: 1, name: 'Maya' }, { id: 2, name: 'Ravi' }]));
export default function App() {
const [result, setResult] = useState({ status: 'loading', students: [], error: '' });
useEffect(() => {
let ignore = false;
const controller = new AbortController();
async function load() {
try {
const response = await fetch(fixture, { signal: controller.signal });
if (!response.ok) throw new Error('Request failed');
const students = await response.json();
if (!Array.isArray(students) || !students.every(s => Number.isInteger(s.id) && typeof s.name === 'string')) throw new Error('Invalid student data');
if (!ignore) setResult({ status: 'success', students, error: '' });
} catch (error) {
if (!ignore) setResult({ status: 'error', students: [], error: error.message });
}
}
load();
return () => { ignore = true; controller.abort(); };
}, []);
return <main><h1>Student directory</h1>
{result.status === 'loading' ? <p role="status">Loading students</p> : result.status === 'error' ? <p role="alert">{result.error}</p> : <ul>{result.students.map(s => <li key={s.id}>{s.name}</li>)}</ul>}
</main>;
}Expected screen and behavior
Initially Loading students. After the local fixture resolves, the directory lists Maya and Ravi.Trace response handling before the data is displayed
React · Fetch an explicit local fixture
import { useEffect, useState } from 'react';
const fixture = 'data:application/json,' + encodeURIComponent(JSON.stringify([{ id: 1, name: 'Maya' }, { id: 2, name: 'Ravi' }]));
export default function App() {
const [result, setResult] = useState({ status: 'loading', students: [], error: '' });
useEffect(() => {
let ignore = false;
const controller = new AbortController();
async function load() {
try {
const response = await fetch(fixture, { signal: controller.signal });
if (!response.ok) throw new Error('Request failed');
const students = await response.json();
if (!Array.isArray(students) || !students.every(s => Number.isInteger(s.id) && typeof s.name === 'string')) throw new Error('Invalid student data');
if (!ignore) setResult({ status: 'success', students, error: '' });
} catch (error) {
if (!ignore) setResult({ status: 'error', students: [], error: error.message });
}
}
load();
return () => { ignore = true; controller.abort(); };
}, []);
return <main><h1>Student directory</h1>
{result.status === 'loading' ? <p role="status">Loading students</p> : result.status === 'error' ? <p role="alert">{result.error}</p> : <ul>{result.students.map(s => <li key={s.id}>{s.name}</li>)}</ul>}
</main>;
}The initial render has status loading. After commit, load starts fetch. A successful response is decoded and its shape checked before any success state is published. The state update then describes the list using stable student IDs as keys.
The error branch stores an explanatory message. Cleanup marks this setup obsolete and requests abort. The ignore check prevents its late completion from publishing state after ownership has ended. A loading page rendered on the server stays loading until the browser mounts and runs this Effect.
Replace the fixture only when an API contract exists
A real application can replace the data URL with its documented endpoint, such as /api/students, but that route must actually be implemented. Authentication, authorization, pagination and response validation belong to that real contract. A demo endpoint string is not evidence that a service is available.
Framework data-loading features can provide route integration, server fetching and caching. Starting every request from a leaf Effect may cause sequential network waterfalls. Keep this lesson’s direct fetch as a mechanism example, then choose the application’s supported data layer deliberately.
Try the school task
React · starting example
import { useEffect, useState } from 'react';
const fixture = 'data:application/json,' + encodeURIComponent(JSON.stringify([{ id: 1, name: 'Maya' }, { id: 2, name: 'Ravi' }]));
export default function App() {
const [result, setResult] = useState({ status: 'loading', students: [], error: '' });
useEffect(() => {
let ignore = false;
const controller = new AbortController();
async function load() {
try {
const response = await fetch(fixture, { signal: controller.signal });
if (!response.ok) throw new Error('Request failed');
const students = await response.json();
if (!Array.isArray(students) || !students.every(s => Number.isInteger(s.id) && typeof s.name === 'string')) throw new Error('Invalid student data');
if (!ignore) setResult({ status: 'success', students, error: '' });
} catch (error) {
if (!ignore) setResult({ status: 'error', students: [], error: error.message });
}
}
load();
return () => { ignore = true; controller.abort(); };
}, []);
return <main><h1>Student directory</h1>
{result.status === 'loading' ? <p role="status">Loading students</p> : result.status === 'error' ? <p role="alert">{result.error}</p> : <ul>{result.students.map(s => <li key={s.id}>{s.name}</li>)}</ul>}
</main>;
}Show an explicit error when returned data is invalid JSON. Use a local invalid fixture so the failure can be reproduced without breaking a service.
Worked answer
The response body below is not JSON, so response.json rejects. The catch branch replaces Loading report with Could not read report. It does not treat an empty object as a valid successful report.
React · JavaScript with JSX
import { useEffect, useState } from 'react';
export default function App() {
const [message, setMessage] = useState('Loading report');
useEffect(() => {
let ignore = false;
async function load() {
try {
const response = await fetch('data:application/json,not-json');
if (!response.ok) throw new Error('Request failed');
await response.json();
if (!ignore) setMessage('Report loaded');
} catch {
if (!ignore) setMessage('Could not read report');
}
}
load();
return () => { ignore = true; };
}, []);
return <main><h1>Report failure</h1><p role="status">{message}</p></main>;
}Expected screen and behavior
Initially Loading report. The invalid JSON fixture then displays Could not read report.Mistakes and boundaries
Abort is supported by fetch through its signal, but not every promise supports cancellation. Preventing an outdated UI update is separate from cancelling the underlying work.
Do not expose secrets or assume a React screen enforces server permissions. A protected directory needs server-side access controls for each request.
A production retry policy should distinguish temporary failures from invalid data or permanent authorization errors. Do not retry every failure forever.
Check your reasoning
React · Fetch an explicit local fixture
import { useEffect, useState } from 'react';
const fixture = 'data:application/json,' + encodeURIComponent(JSON.stringify([{ id: 1, name: 'Maya' }, { id: 2, name: 'Ravi' }]));
export default function App() {
const [result, setResult] = useState({ status: 'loading', students: [], error: '' });
useEffect(() => {
let ignore = false;
const controller = new AbortController();
async function load() {
try {
const response = await fetch(fixture, { signal: controller.signal });
if (!response.ok) throw new Error('Request failed');
const students = await response.json();
if (!Array.isArray(students) || !students.every(s => Number.isInteger(s.id) && typeof s.name === 'string')) throw new Error('Invalid student data');
if (!ignore) setResult({ status: 'success', students, error: '' });
} catch (error) {
if (!ignore) setResult({ status: 'error', students: [], error: error.message });
}
}
load();
return () => { ignore = true; controller.abort(); };
}, []);
return <main><h1>Student directory</h1>
{result.status === 'loading' ? <p role="status">Loading students</p> : result.status === 'error' ? <p role="alert">{result.error}</p> : <ul>{result.students.map(s => <li key={s.id}>{s.name}</li>)}</ul>}
</main>;
}Choose the answer that follows the data and the interaction, then read the explanation for each alternative.
Carry the idea forward
Design loading, success and error states and verify the response before publishing data. Continue with obsolete request results and application data layers.
Read the primary reference: React: fetching data in Effects.
Reaching the recap and completing the quizzes are separate milestones.