# Why You Still Need to Know Fallback Strategies When AI Writes Your Code

> Source: <https://dev.to/mitar_nik/why-you-still-need-to-know-fallback-strategies-when-ai-writes-your-code-3io5>
> Published: 2026-09-07 08:36:34+00:00

Say you ask an AI assistant to build a settings page that pulls user preferences from an API. Odds are you'll get clean code back. Typed, well-named, properly async. Odds are just as good you'll also get a page that shows a blank white screen the moment that API call fails.

Nobody told the model to think about failure. And it won't, because that's not what it was asked for.

**Side note on the examples:** I'm using C# with Polly for the backend snippets and React for the frontend ones, mostly because that's what I've been shipping lately. None of this is language-specific. Swap in `resilience4j`<sup>1</sup> for Java, `tenacity`<sup>2</sup> for Python, `cockatiel`<sup>3</sup> for Node, whatever your stack's equivalent is. The patterns, and more importantly the point about AI not volunteering them, apply everywhere.

That's the whole problem in one sentence, honestly, but let's unpack it, because it matters more now than it did back when "AI helps you code" meant autocomplete finishing a line you'd already started, not generating an entire feature from a prompt. And that shift is a lot more recent than it feels. We're talking two, maybe three years, not some distant chapter of programming history.

Here's the thing about how these models are trained and how we tend to prompt them: we ask for outcomes. "Fetch the user's orders." "Add a login form." "Call this payment endpoint." The model gives you exactly that, and it does the happy path well. Correct types, sensible naming, reasonable structure. I'm not knocking the code quality.

What it doesn't do, unless you specifically ask, is sit back and think "what happens when this fails at 2am for a user on a flaky hotel wifi." That's not a knowledge gap either. Ask any of the big models directly "what should I do if this API call fails" and you'll get a perfectly good answer, retries, timeouts, fallback UI, the works. The model knows this stuff. It just doesn't volunteer it unless the prompt (or the surrounding code it's imitating) nudges it there.

So the failure mode isn't "AI doesn't know about resilience." It's "AI answers the question you asked, and most of us don't ask about failure." That's a prompting and reviewing problem, and it's on us to close it.

I think this is actually a bigger deal than it looks. When you write code yourself, line by line, you naturally hit the "wait, what if this throws" moment because you're forced to think through the logic as you type it. When code arrives fully formed and looks right, that friction disappears. It's easy to accept a block of working-looking code without ever mentally running the failure scenarios. The review step that used to happen automatically, as a side effect of writing, now has to happen deliberately, as a separate step.

And the honest reason this deserves its own conversation now is that the scope of what gets handed to you changed fast, faster than the "be careful with AI-generated code" advice around it has caught up. Autocomplete finishing the rest of a line you were already writing still kept you in the driver's seat, you were thinking through the function, the model was just saving keystrokes, and that meant you were still the one who'd naturally wonder what happens if the call fails, because you were still writing the call. What's normal now is different in kind, not just degree: work out the requirements, design a prompt around it and hand it off to AI, a new component, sometimes a small pull request's worth of changes across several files, done. Nobody sat there typing the unhappy path and skipping it, because nobody sat there typing it at all. This has become the default way a lot of code gets written, quietly, over roughly the last couple of years, not some hypothetical future workflow. That's exactly why the review habit has to be deliberate now. There's no longer a "wait, what if this throws" moment built into the act of writing, because for a growing share of code, nobody's writing it in that sense anymore.

Picture a generated integration with a third-party shipping rate API. It works perfectly in every demo, every staging test, every code review. Nobody thinks twice about it because it looks done. Then one day the shipping provider has a rough Tuesday, degraded response times, no full outage, just slow. No timeout was ever set anywhere in the chain, so requests pile up, connections get exhausted, and the checkout page for the entire store goes down with it. Not because the shipping API was fully down, just because nothing was watching the clock or the connection pool.

Nobody was being lazy here. The code compiled, it passed every test that existed, and it did exactly what was asked of it. The problem is that "call the shipping API and return the rate" was the entire spec, spoken and unspoken, and nobody added "and don't let this take the whole service down with it." That gap is invisible until the day it isn't.

This one applies equally to backend and frontend retries, so it's worth covering once here rather than twice later. A few terms worth being precise about, since they get used interchangeably in conversation when they really shouldn't be:

None of these is universally "correct." A constant short delay might be all you need for a quick UI retry where a user is actively waiting and you don't want them staring at a spinner for eight seconds. Exponential backoff earns its keep more on backend-to-backend calls where a shared, struggling dependency needs the breathing room. Jitter matters most once you have enough concurrent clients that synchronized retries could pile up into a problem of their own. Which combination fits depends on what's actually failing and who's waiting on the other end, not on which one shows up first in a blog post.

None of this is new. Distributed systems have always needed to handle timeouts, downstream outages, and partial failures. What's new is how tempting it is to skip it when the generated code compiles and passes the demo.

A few patterns worth actually asking for by name, because "handle errors" is too vague to get you anything useful. In .NET, Polly is the library that already does this well, so there's really no excuse not to reach for it.

**Timeouts, first, before anything else.** An awful lot of production incidents trace back to a call with no timeout at all, quietly waiting forever on a socket that will never respond. This should be the default assumption for every outbound call, not something you add after the first incident.

``` js
var timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromSeconds(5));
```

**Retries**, built on top of whichever backoff strategy from the primer above actually fits the situation. For backend-to-backend calls that's usually exponential backoff, since it's the one that backs off fast enough to relieve pressure on a struggling shared dependency:

``` js
var retryPolicy = Policy
    .Handle<HttpRequestException>()
    .Or<TimeoutException>()
    .WaitAndRetryAsync(
        retryCount: 3,
        sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)));

await retryPolicy.ExecuteAsync(() => inventoryClient.GetStockAsync(productId));
```

The snippet above is plain exponential backoff, no jitter, kept simple for the example. If you want jitter without doing the random-number math by hand, the `Polly.Contrib.WaitAndRetry` package ships a decorrelated jitter generator you can drop straight into `sleepDurationProvider`.

Worth mentioning here: if you're calling a third-party API that returns a `Retry-After` header, respect it instead of guessing your own backoff. A partner's rate limiter telling you to wait 12 seconds and you retrying after 2 anyway is just going to make the throttling worse for everyone hitting that API, not just you.

**Circuit breakers** so a struggling dependency doesn't take the whole request pipeline down with it, which is exactly the failure mode from the shipping story above. If your payment provider is timing out, you want to stop calling it for thirty seconds and serve a fallback response, not queue up five thousand more requests that all time out too.

``` js
var circuitBreakerPolicy = Policy
    .Handle<HttpRequestException>()
    .CircuitBreakerAsync(
        exceptionsAllowedBeforeBreaking: 5,
        durationOfBreak: TimeSpan.FromSeconds(30));
```

**Bulkheads**, which are less talked about but genuinely important once you have more than a couple of external dependencies. The idea is to isolate the resources (thread pool, connection pool) used to call one dependency so that if it saturates, it doesn't starve the calls going to everything else.

``` js
var bulkheadPolicy = Policy.BulkheadAsync(maxParallelization: 10, maxQueuingActions: 20);
```

**Graceful degradation with a fallback value**, which is the one I see skipped most often. If your recommendation service is down, show generic popular items instead of erroring the whole page. Polly has a `FallbackAsync` policy for exactly this, and you can wrap it around the retry, timeout, and circuit breaker so the whole chain degrades cleanly instead of throwing all the way up to your controller:

``` js
var fallbackPolicy = Policy<IEnumerable<Product>>
    .Handle<Exception>()
    .FallbackAsync(fallbackValue: GetPopularItemsFallback());
```

Putting all four policies together

This is what it looks like once timeout, retry, circuit breaker, and fallback are wrapped into a single pipeline, outermost first:

``` js
var resilientCall = fallbackPolicy
    .WrapAsync(retryPolicy)
    .WrapAsync(circuitBreakerPolicy)
    .WrapAsync(timeoutPolicy);

var products = await resilientCall.ExecuteAsync(() =&gt; recommendationService.GetAsync(userId));
```

Order matters here. Fallback sits on the outside so it catches whatever bubbles up from everything inside it, timeout sits on the inside so it applies to each individual attempt, not the retries as a whole.

I've asked AI tools to build API integrations dozens of times, and the timeout, the retry policy, the circuit breaker, the bulkhead, the fallback value, none of it shows up unless I ask for it by name. It's not that the model can't write it, and it's not that it doesn't know Polly exists. It's that "fetch the user's cart from the inventory service" doesn't imply "and also assume the inventory service will occasionally be slow, occasionally down, and occasionally rate-limiting you."

Frontend resilience gets even less attention, maybe because a failed API call in the backend throws a loud error while a failed fetch in a React component can just render nothing, and nobody notices until a user complains.

**Error boundaries**, so one broken component doesn't blank the entire page:

```
class ErrorBoundary extends React.Component {
  state = { hasError: false };
  static getDerivedStateFromError() {
    return { hasError: true };
  }
  render() {
    if (this.state.hasError) return this.props.fallback;
    return this.props.children;
  }
}
```

**Loading, empty, and error states as first-class UI**, not an afterthought bolted on after QA finds the bug. If you ask for "a table of orders," you'll usually get the table and the happy path. You have to separately ask "what does this look like while loading, if the list is empty, and if the request fails." And while we're here, a skeleton screen that mirrors the eventual layout genuinely does read as faster to users than a spinner, even at the same load time. Small thing, but it's the kind of detail that never shows up unless someone asks for it on purpose.

**Optimistic UI with rollback** for things like likes or form submissions, where you update the UI immediately but need a path back if the server rejects the change. It's a nice touch that makes an app feel instant, but only if the rollback path actually exists. An optimistic update with no rollback is just a UI that lies to the user for a few seconds before quietly reverting itself, or worse, not reverting at all.

**Retry logic for flaky requests**, and this is the one where I'd actively push back on doing it ad hoc. The moment more than one component in your frontend needs "call this endpoint again after a delay if it fails," that's a sign you shouldn't be writing that logic inline each time. Build a small retry abstraction once, something any component or hook can reach for, that bakes in the standard strategies from the primer earlier (fixed delay, linear, exponential, with or without jitter, maybe a max-attempts cutoff) and lets the caller just pick whichever one fits and hand it a request function. I'm deliberately not pasting the implementation here, it's not complicated, but the point is that once it exists, nobody on the team has to reinvent "wait and retry" from scratch, and nobody quietly ships a component with zero retry handling just because writing it inline felt like too much friction in the moment.

**Stale-while-revalidate style caching**, where you show the last known-good response immediately while a fresh request happens in the background, and only replace what's on screen once the new data actually arrives. Libraries like React Query or SWR give you most of this for free, but it's worth understanding what they're doing under the hood, because "just use React Query" isn't a fallback strategy by itself if nobody configures what happens on repeated failure.

**Feature flags with sane defaults**, so that if your flag service is unreachable, the app falls back to a known default state rather than crashing on a missing config value. This one's easy to miss because it usually only shows up in an incident review, after the flag service had a bad day and half the app rendered blank because a boolean came back `undefined` instead of `false`.

**Offline and stale-data handling**, especially for anything that touches `localStorage`, service workers, or cached API responses. Showing three-hour-old cached data with a small "may be outdated" note beats a blank screen every time.

None of these are exotic. They're just easy to leave out when the code you're reviewing already looks finished.

One thing I've started doing more deliberately, precisely because generated code looks so confident: actually breaking the dependency on purpose and watching what happens. Kill the network tab in dev tools mid-request. Point a service at a dead port. Throw an exception manually inside the try block before you ship. It sounds obvious written out like this, but I've caught more than one case where the fallback code was there, it just had a bug in it that meant it never actually triggered, because nobody had ever forced the failure path to run. A fallback you've never tested is a fallback you're hoping works, not one you know works.

I don't think the lesson here is "don't trust AI-generated code." I use it every day and it's genuinely good at what it does. The lesson is that the skill of "where does this break, and what should happen when it does" hasn't gone away, it's just moved from something you did while typing to something you have to do while reviewing and prompting. And it moved there quietly, in the space of a couple of years, which is probably why so many teams are still reviewing AI-generated code with the same checklist they used for hand-written code, correctness, style, tests, and nothing that specifically asks "would this survive a bad network day."

Concretely, that means:

The models aren't going to start guessing which parts of your app need to survive a bad network day. That's still your job, arguably it's becoming the more important half of the job, since the code-writing part keeps getting faster and the "does this hold up when things go wrong" part doesn't get any easier just because a machine wrote the happy path.

Fallbacks were never really about the code being hard to write. They were about remembering they're needed in the first place. That part hasn't gotten any easier. If anything, it's the one skill AI can't do for you until you ask.

[resilience4j](https://resilience4j.readme.io/) - fault tolerance library for Java, built around the same policy-wrapping idea as Polly. ↩

[tenacity](https://tenacity.readthedocs.io/) - general-purpose retrying library for Python. ↩

[cockatiel](https://github.com/connor4312/cockatiel) - resilience and transient-fault-handling library for Node.js, explicitly modeled after Polly. ↩

[Exponential Backoff And Jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/) - the AWS Architecture Blog post that popularized full jitter vs. equal jitter, with benchmarks showing the difference under real contention. ↩
