Course / Module 11

Fallbacks and graceful degradation

Design what happens when understanding, retrieval, tools, models or channels fail: a fallback ladder, resilient calls, honest wording, handoff when no one is available and tests that break things on purpose.

13 min study timeDesignCodeOpsLast reviewed Markdown version

In short

  • Plan for failure first: every step needs a safe next step.
  • Fail safe, fail visible and fail forward: never a loop or a dead end.

What you will learn

  • Classify the ways an assistant fails and match each to a fallback
  • Design a fallback ladder that avoids loops and dead ends
  • Make model, tool and retrieval calls resilient with retries, timeouts and failover
  • Write honest, useful fallback messages for chat and voice
  • Test fallbacks by injecting failures

Everything fails somewhere

Assistants fail in many ways, and the failure users see is usually the one nobody designed. The McDonald's drive-thru trial is a clear example: when the system misheard, a person had to step in. A fallback is the planned response to a failure. Good ones keep the user moving and the system safe.

The ways it fails

Failure Example Fallback direction
Not understood "Sorry, I didn't get that" Clarify, offer choices, show examples
Low confidence Two likely intents Ask a disambiguating question
Nothing retrieved The knowledge base has no answer Say so, point to the right source, offer a person
Tool error or timeout The order system is down Retry safely, explain, offer a callback
Model error or outage Rate limit or provider incident Retry, fail over to another model, degrade
Policy block A guardrail stopped the reply Safe alternative wording, scoped help
User frustration Repeated "agent", anger Hand off promptly
Unsupported language The user switches language Detect, confirm, switch or offer a person
Channel failure Noisy audio, failed upload Switch modality or channel (module 21)

Three principles

  • Fail safe: when unsure, do the least risky thing. Don't guess on money or personal data.
  • Fail visible: tell the user what happened and what comes next. Hidden failures breed repeats and complaints.
  • Fail forward: every fallback ends with a next step. Never a loop, never a dead end.

The fallback ladder

Escalate through increasingly different strategies as failures accumulate.

Retry safelyClarify with a specific questionOffer two or three choicesSwitch mode: simpler flow or another channelStatic or cached answerHuman handoffCallback or ticket

Each rung changes the approach. Count failures so you move up instead of repeating.

python
FALLBACKS = [
    ("clarify", "Could you tell me a bit more? For example, are you asking about a refund, a delivery or an account change?"),
    ("choices", "I can help with: 1) a return, 2) an order status, or 3) talking to a person. Which would you like?"),
    ("handoff", "I'm not getting this right. Let me connect you with a person, and I'll pass along what we've discussed."),
]

def next_fallback(ctx: dict) -> tuple[str, str]:
    """ctx['fails'] counts consecutive failures in this conversation."""
    ctx["fails"] = ctx.get("fails", 0) + 1
    index = min(ctx["fails"] - 1, len(FALLBACKS) - 1)
    return FALLBACKS[index]

def on_success(ctx: dict) -> None:
    ctx["fails"] = 0        # reset only on genuine progress

Make calls resilient

Timeouts. Every external call needs one. A call without a timeout is a hung conversation.

Retries with backoff and jitter. Retry transient errors a few times, waiting longer each time, with random jitter so many clients don't retry at once. Don't retry errors that won't change, such as invalid arguments.

Idempotency. A retry after a timeout may repeat the action. Use idempotency keys for writes so charging, cancelling or sending can't happen twice.

Circuit breakers. After repeated failures, stop calling the broken dependency for a while and use the fallback straight away instead of making every user wait.

Failover. Keep a tested secondary model, ideally from another provider, and a smaller model as a degraded mode (module 4). Log which model served each turn.

python
import random, time

def call_with_fallback(primary, secondary, payload, retries=2, base_delay=0.4):
    """Try the primary a few times, then fail over. Returns (result, model_used)."""
    for attempt in range(retries + 1):
        try:
            return primary(payload, timeout=8), "primary"
        except TransientError:
            time.sleep(base_delay * (2 ** attempt) + random.uniform(0, 0.2))   # backoff with jitter
        except PermanentError:
            break                                                              # retrying will not help
    try:
        return secondary(payload, timeout=8), "secondary"
    except Exception:
        return None, "none"          # caller shows the safe static fallback and hands off

Say it well

Fallback messages are some of the most-read text in your product. Good ones are honest, specific and forward-moving.

Nothing found: honest and useful
Can I return a jacket I bought in a store?
I only have the policy for online orders, so I can't answer that reliably. Store returns are handled by the store policy, and I can connect you to someone who can confirm it. Would you like that?
A tool is down
Where is order A1234?
I can't reach our order system right now. I haven't lost your request. I can text you the status as soon as it's back, or connect you to an agent. Which would you prefer?

For voice, keep fallbacks short, avoid repeating the same prompt, and offer a keypad or human option early (module 19).

When the human isn't available

A handoff is itself a dependency that can fail. Plan for it:

  1. Check availability before promising a handoff.
  2. If no one is available, say so plainly with hours or expected wait.
  3. Offer a callback or a ticket, capture contact details and a summary, and confirm what will happen next.
  4. For urgent or safety-critical topics, give the appropriate emergency or priority route immediately.

A bug is not a policy question

The Cursor case study shows a subtler fallback problem. When users reported unexplained logouts, the support bot answered with an invented policy instead of treating it as a possible defect. Teach the assistant to recognize anomaly reports ("this stopped working," "I was logged out") and route them to a person or an engineering ticket, with logs, rather than explaining system behavior it has no source for.

Test fallbacks by breaking things

  • Inject failures: kill a tool, add latency, return malformed data, exhaust a rate limit, switch off the primary model.
  • Check the user experience: the message, the next step, whether context survived.
  • Simulate repeated misunderstanding and confirm the ladder advances.
  • Test the handoff with and without available agents.
  • Spec them like any behavior (module 15), and run chaos-style tests in staging.

Monitor fallback rate by cause and by step. A rising "not found" rate points to content gaps (module 17), and rising tool timeouts point to an integration problem.

Key takeaways

  • Plan for failure first: every step needs a safe next step.
  • Fail safe, fail visible and fail forward: never a loop or a dead end.
  • Retry what is safe to retry, and make writes idempotent.
  • A human handoff needs its own fallback for when no one is available.

Knowledge check