Fallbacks Are Product Decisions
Fallbacks are product decisions
A fallback in a mutation path is product policy. It decides what the system may do on the user's behalf when the intended target is missing.
For reads, a loose fallback can be acceptable: use cached data, show partial results, retry a secondary provider. For writes and real-world side effects, loose fallback is dangerous. "Use some available target" can mean play music on the wrong device, edit the wrong record, charge the wrong account, or message the wrong person.
The rule
Fail closed on mutation targets.
If the user intended a specific target, the system can use exact matches, explicitly configured aliases, or strongly owned local identity. It should not silently pick an unrelated available target because that target happens to be valid.
What to do instead
- Return an actionable error that names what was intended and what was visible.
- Offer a repair command, diagnostic command, or explicit selection step.
- Keep dry-run and commit on the same selection path so fallback behavior is visible before the mutation. See Same-Code-Path Preview.
- Treat fallback rules as product policy, not incidental helper code.
Failure is not evidence
An unavailable model, malformed response, timeout, or failed tool call is an operational failure. It is not evidence that a policy condition is false, unknown, or absent. Converting one into the other can make a deterministic policy produce a confident but invalid result.
Keep failure states explicit, then let product policy decide whether to retry, fail closed, ask for human review, or return an honest unavailable result. See Governed Agent Architecture.
Spotuify example
In Spotuify, pressing play in the TUI should target the daemon's embedded librespot device or a configured/active Spotify Connect device. If the daemon's device is not visible, falling through to another unrestricted device is surprising and wrong: it can start playback in some other room or on some other client.
The code now encodes that policy: device selection prefers the daemon-owned device id, active unrestricted devices, configured names, and spotuify/librespot name markers. If none match, it returns no target.
Where this generalizes
- Playlist edits: do not edit the first matching playlist if the intended playlist id is unavailable.
- Payments: do not charge a backup card unless the user explicitly opted into that policy.
- Deployments: do not deploy to the next reachable environment if production is unavailable.
- Notifications: do not send to an alternate channel unless the user selected escalation.
- Rate limiting: if the durable backend is unavailable, a weaker in-memory guardrail can be acceptable because it preserves availability without changing the user's intended mutation target.