Error Handling and Error Boundaries
Level: Beginner
- try/catch blocks for handling synchronous errors
- Error states in useState for API failures
- Displaying error messages to users
- Error Boundaries for catching render errors
- Fallback UI when things break
- Recovering from errors gracefully
- Network error handling in fetch/axios
- SMS error scenarios: failed API calls, invalid data, network timeouts
Why This Matters
Error Handling and Error Boundaries is part of building maintainable React applications. You will use it when creating student dashboards, forms, tables, API-connected screens, routing flows, and reusable UI components.
Errors happen. Handle them gracefully so your app doesn't crash and shows helpful messages to users.
The Problem
Beginners often write React code that works for a small demo but becomes difficult when data, forms, API calls, and reusable components grow. This lesson explains Error Handling and Error Boundaries in a way that helps you build predictable UI for real .NET Web API projects.
Error States in Components
Try/Catch for Async
function StudentDetail({ studentId }) {
const [student, setStudent] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const fetchStudent = async () => {
try {
const response = await fetch(`/api/students/${studentId}`);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
setStudent(data);
setError(null);
} catch (err) {
setError(err.message);
setStudent(null);
}
};
fetchStudent();
}, [studentId]);
if (error) {
return (
<div className="error">
<p>Failed to load student: {error}</p>
<button onClick={() => window.location.reload()}>Retry</button>
</div>
);
}
if (!student) return <p>Loading...</p>;
return <div>{student.name}</div>;
}
Error State Without Async
function StudentMarksValidator() {
const [marks, setMarks] = useState("");
const [error, setError] = useState("");
const handleChange = (e) => {
const value = e.target.value;
setMarks(value);
// Clear error on new input
setError("");
};
const handleSubmit = (e) => {
e.preventDefault();
// Validate
if (!marks) {
setError("Marks cannot be empty");
return;
}
const num = parseFloat(marks);
if (isNaN(num)) {
setError("Marks must be a number");
return;
}
if (num < 0 || num > 100) {
setError("Marks must be between 0 and 100");
return;
}
// No errors, submit
console.log("Valid marks:", num);
};
return (
<form onSubmit={handleSubmit}>
<input
value={marks}
onChange={handleChange}
placeholder="Enter marks"
/>
{error && <p className="error">{error}</p>}
<button type="submit">Submit</button>
</form>
);
}
Error Boundaries (Catch Entire Subtree)
Error Boundaries catch errors in child components. They're like try/catch for components.
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error("Error caught:", error, errorInfo);
// Log to error tracking service (Sentry, etc.)
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}
// Usage
function App() {
return (
<ErrorBoundary>
<StudentDashboard />
<TeacherList />
</ErrorBoundary>
);
}
If StudentDashboard or TeacherList crashes, the Error Boundary catches it.
Practical Error Handling Examples
Example 1: API Call with Full Error Handling
function ExamResults({ studentId }) {
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
const fetchResults = async () => {
try {
const response = await fetch(
`/api/students/${studentId}/exams`
);
// Network error
if (!response.ok) {
if (response.status === 404) {
throw new Error("Student not found");
}
if (response.status === 500) {
throw new Error("Server error. Please try again later");
}
throw new Error(`Error: ${response.status}`);
}
const data = await response.json();
if (isMounted) {
setResults(data);
setError(null);
}
} catch (err) {
if (isMounted) {
setError(err.message);
setResults([]);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
fetchResults();
return () => {
isMounted = false;
};
}, [studentId]);
if (loading) return <p>Loading results...</p>;
if (error) return <p className="error">{error}</p>;
if (results.length === 0) return <p>No exam results found</p>;
return (
<table>
<tbody>
{results.map(result => (
<tr key={result.id}>
<td>{result.examName}</td>
<td>{result.marks}</td>
</tr>
))}
</tbody>
</table>
);
}
Example 2: Form Submission Error
function FeePaymentForm({ studentId }) {
const [formData, setFormData] = useState({ amount: "", mode: "Online" });
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setError(null);
setLoading(true);
try {
const response = await fetch("/api/fee-payment", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
studentId,
amount: parseFloat(formData.amount),
mode: formData.mode
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(
errorData.message || "Payment failed. Please try again."
);
}
const data = await response.json();
setSuccess(true);
setFormData({ amount: "", mode: "Online" });
// Show success message for 3 seconds
setTimeout(() => setSuccess(false), 3000);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
setError(null); // Clear error when user types
};
return (
<form onSubmit={handleSubmit}>
{success && (
<div className="success">Payment submitted successfully!</div>
)}
{error && (
<div className="error">
<p>{error}</p>
</div>
)}
<input
name="amount"
type="number"
value={formData.amount}
onChange={handleChange}
placeholder="Amount"
disabled={loading}
/>
<select
name="mode"
value={formData.mode}
onChange={handleChange}
disabled={loading}
>
<option value="Online">Online</option>
<option value="Cash">Cash</option>
<option value="Cheque">Cheque</option>
</select>
<button type="submit" disabled={loading}>
{loading ? "Submitting..." : "Submit Payment"}
</button>
</form>
);
}
Example 3: Multiple Dependent API Calls
function StudentFullProfile({ studentId }) {
const [profile, setProfile] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchProfile = async () => {
try {
// Fetch student
const studentRes = await fetch(`/api/students/${studentId}`);
if (!studentRes.ok) throw new Error("Student not found");
const student = await studentRes.json();
// Fetch marks
const marksRes = await fetch(`/api/students/${studentId}/marks`);
const marks = marksRes.ok ? await marksRes.json() : [];
// Fetch fees
const feesRes = await fetch(`/api/students/${studentId}/fees`);
const fees = feesRes.ok ? await feesRes.json() : null;
setProfile({ student, marks, fees });
setError(null);
} catch (err) {
setError(err.message);
setProfile(null);
} finally {
setLoading(false);
}
};
fetchProfile();
}, [studentId]);
if (loading) return <p>Loading profile...</p>;
if (error) return <p className="error">Error: {error}</p>;
if (!profile) return <p>No data</p>;
return (
<div>
<h1>{profile.student.name}</h1>
<p>Marks: {profile.marks?.length || 0}</p>
<p>Fees Status: {profile.fees?.status || "N/A"}</p>
</div>
);
}
User-Friendly Error Messages
Map HTTP status codes to user-friendly messages:
const getErrorMessage = (status) => {
const messages = {
400: "Invalid request. Please check your input.",
401: "You need to log in first.",
403: "You don't have permission to do this.",
404: "The requested resource was not found.",
409: "This resource already exists.",
500: "Server error. Please try again later.",
503: "Service is temporarily unavailable."
};
return messages[status] || "An unexpected error occurred.";
};
// Usage
const response = await fetch(url);
if (!response.ok) {
throw new Error(getErrorMessage(response.status));
}
Key Takeaways
- Always handle errors in async operations
- Show error messages to users
- Use Error Boundaries for catastrophic failures
- Validate data before sending to backend
- Log errors for debugging
- Next: API integration
- Ignoring errors — User sees nothing, thinks app is broken
- Generic error messages — "Something went wrong" doesn't help
- No retry mechanism — Let users try again
- Logging to console only — Won't see production errors
Use services like Sentry to track errors in production:
componentDidCatch(error, errorInfo) {
// Send to Sentry
Sentry.captureException(error);
}
Use ChatGPT, Claude, or Copilot to go deeper on Error Handling. Try these prompts:
"What's the difference between error states and Error Boundaries?""How would you handle a 404 vs. a 500 error differently?""When should you show errors to users vs. log them?""Quiz me on error handling"
💡 Tip: After reading this article, paste your own code into AI and ask "What could go wrong here and why?" — fastest way to find edge cases and deepen understanding.
Quick Definitions
- Error Handling and Error Boundaries - The main React concept explained in this lesson.
- Component - A reusable function that returns UI.
- Props/state/effects - Core React ideas used to pass data, remember data, and run side effects.
- Real project usage - How this appears in forms, dashboards, routes, and API-connected pages.
Common Mistakes
- Copying React code without understanding data flow
- Mutating arrays or objects directly instead of creating new values
- Forgetting keys, dependencies, loading states, or error states where needed
- Putting too much logic in one component
- Not testing the screen with realistic School Management System data
Practice Task
Create a small React example using Error Handling and Error Boundaries. Keep it focused on one School Management System screen.
Suggested practice:
- Build a small component or page for students, attendance, marks, or fees.
- Pass realistic data into the component.
- Add one success state and one empty/error state where relevant.
- Explain the data flow in your own words.
- Rebuild the same example once without looking at the article.
Quick Revision
| Question | Answer |
|---|---|
| What is the main idea? | Understand and apply Error Handling and Error Boundaries in React. |
| Where is it used? | Student dashboards, forms, tables, routes, and API-connected screens. |
| What should beginners focus on? | Clear components, predictable data flow, and small examples. |
| What is the best debugging habit? | Check props, state, render output, and browser console step by step. |
Error Handling and Error Boundaries is a React concept used to build clear, reusable, and predictable user interfaces. I would explain the problem it solves, show a small component example, and mention a common mistake beginners should avoid.
It is used in screens like student lists, admission forms, attendance dashboards, marks reports, routing pages, and API-connected admin panels.