ProgramGuru

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.

  1. Understand

    Represent request status clearly.

  2. Build

    Check HTTP success and parse the expected response.

  3. Check

    Distinguish local fixture data from a real service.

A request has more than one outcome

  1. Pending

    Display a loading message while no result exists.

  2. Receive

    Check the response and decode the expected data.

  3. Settle

    Show the directory or a recoverable failure message.

Read this relationship alongside the complete code. This diagram explains the flow; it does not execute React.

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.

A fetch receives HTTP 404. What should the code check before treating it as successful JSON?
Choose one answer. You can try again.

Answer and explanation

response.ok or the documented status condition.

Correct. An HTTP error response is not automatically a rejected fetch promise.

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.

    Next lesson: Preventing Stale Responses: Keep Results with Their Selection →