Designing a Resilient UI: Handling Failures Gracefully in Frontend Applications

posted 3 min read

Introduction

No matter how robust our applications are, failures are inevitable. APIs may fail, networks can be unreliable, and unexpected errors will always creep in. As frontend engineers, our job isn’t just to build beautiful UIs—it’s to design experiences that remain functional and user-friendly even when things go wrong.

In this post, we’ll explore how to build resilient UIs by anticipating failures, handling them gracefully, and ensuring a smooth user experience even in adverse conditions.

1. Embrace the ‘Fail Fast, Recover Gracefully’ Mindset

A resilient UI doesn’t just detect failures—it recovers from them efficiently. Instead of letting users get stuck with a broken page, we should:

  • Detect errors early (fail fast)
  • Provide meaningful feedback (graceful recovery)
  • Allow users to retry or recover

Example: If a data-fetching API fails, show a retry button instead of an empty screen.

{error ? (
  <div>
    <p>Oops! Something went wrong.</p>
    <button onClick={retryFetch}>Retry</button>
  </div>
) : (
  <DataComponent data={data} >
)}

2. Use Skeletons & Optimistic UI for Seamless Feedback

Ever waited for content to load and felt frustrated by a blank screen? Avoid this by implementing:

  • Skeleton loaders instead of spinners to give users an immediate
    visual cue.
  • Optimistic UI updates, where actions like "liking" a post
    update the UI instantly while confirming with the server in the
    background.

Example of Optimistic UI for a Like Button:

const [liked, setLiked] = useState(false);

const handleLike = async () => {
  setLiked(true); // Instantly update UI
  try {
    await api.likePost(); // Server confirmation
  } catch (error) {
    setLiked(false); // Rollback if failed
  }
};

This keeps the UI responsive while preventing frustration.

3. Provide Meaningful Error Messages

Users hate vague error messages. Instead of "Error: Request failed", provide actionable information:
???? Bad UX: "Something went wrong. Try again later."
✅ Good UX: "Unable to fetch data. Check your internet connection or try again."

Pro Tip: Use error boundaries in React to catch unexpected UI crashes.

4. Implement Smart Retry & Fallback Strategies

If a request fails, don’t just give up—implement smart retry logic with exponential backoff.

Example: Retrying API Calls with Exponential Backoff

const fetchWithRetry = async (url, retries = 3, delay = 1000) => {
  try {
    return await fetch(url);
  } catch (error) {
    if (retries > 0) {
      await new Promise((res) => setTimeout(res, delay));
      return fetchWithRetry(url, retries - 1, delay * 2); // Exponential backoff
    }
    throw error;
  }
};

For non-critical failures (e.g., a sidebar not loading), show fallback content rather than breaking the entire UI.

5. Design for Offline & Slow Networks

Not all users have blazing-fast internet. Progressive Web Apps (PWAs) offer offline support, but even regular apps can:
- Cache important data (e.g., using IndexedDB or localStorage)
- Display last known state when offline
- Use lazy loading for non-essential features

Example: Handling Offline Mode Gracefully

const isOnline = navigator.onLine;
return isOnline ? <Dashboard > : <p>You’re offline. Showing cached data.</p>;
Tip: Use meaningful error messages that guide users towards a solution, like "Unable to fetch data. Check your internet connection or try again." This helps users feel empowered instead of frustrated.
Caution: Don’t overuse retries—too many retries can put unnecessary strain on your server and frustrate users. Always set a limit to prevent endless retries.
Note: Skeleton loaders provide a better user experience than spinners by giving users an immediate visual cue that content is loading, reducing perceived wait times.

Conclusion: Build for the Worst, Deliver the Best

A truly great UI isn’t just about aesthetics—it’s about reliability. By designing with resilience in mind, we create applications that handle failures smoothly, keeping users engaged and frustration-free.

A smooth sea never made a skilled sailor.
— Franklin D. Roosevelt

So let’s embrace failures, design for them, and build frontends that can withstand the unexpected.

What strategies have you used to make your UI more resilient? Drop your thoughts in the comments!

If you read this far, tweet to the author to show them you care. Tweet a Thanks
Stanley, its a great post! I love the focus on optimistic UI and meaningful error messages—they make such a difference for user experience.

Have you tried using circuit breaker patterns in the UI to handle repeated API failures? Curious to hear your thoughts on balancing resilience with performance!"
Thanks for the kind words!

Great question on circuit breakers! While they’re more common in backend systems, they can be incredibly useful in frontend too—especially for preventing excessive API failures from degrading performance. I’ve explored using circuit breakers to limit repeated failed requests, trigger a cooldown, and show fallback UI instead of hammering an unstable API.
The challenge is balancing resilience vs. responsiveness—too aggressive, and users might see stale data; too lenient, and failures pile up.

More Posts

Handling Errors and Job Lifecycles in Rails 7.1: Mastering `retry_on`, `discard_on`, and `after_discard`

ShahZaib - Nov 24, 2024

Python Exception Handling

Abdul Daim - Mar 4, 2024

The quiet, pervasive devaluation of frontend - My answer to Josh Collinsworth's article.

Michael Di Prisco - Oct 12, 2024

Mastering React: A Mindset for Component-Centric Development

Losalini Rokocakau - May 3, 2024

How Check Memory Leaks in React?

Martins Gouveia - Feb 5
chevron_left