Foundation Models GenerationError: Complete Handling Reference for Production iOS Apps
- Author
- Ehsan Azish · 3NSOFTS
- Updated
- June 2026
- Read time
- 14 min read
- Level
- Intermediate
- Platform
- iOS 26+, Foundation Models, async/await
Implementation Notes
- ~/ What broke: Opaque Foundation Models errors reached users as generic failures.
- ~/ What to do: Map Foundation Models failures to deterministic UI states and useful recovery paths.
When a Foundation Models generation fails, you get a LanguageModelSession.GenerationError. If you only catch { } it generically, you'll see opaque messages like:
The operation couldn't be completed.
(FoundationModels.LanguageModelSession.GenerationError error 4.)
That tells the user nothing and tells you nothing. This is the reference that maps every case to its meaning, its recoverability, and the action you should take — so your error handling is exhaustive instead of a single catch-all.
The cases
GenerationError is an enum. Each case carries a Context with a debugDescription. Here's the full surface and how to treat each one.
exceededContextWindowSize
What it means: The session transcript (instructions + all prior turns + this prompt) exceeded the model's context window. Recoverable: Yes, but not in the catch block — you must start a new session. Action: Summarize-and-carry, or hard reset. See the dedicated guide. User copy: None — recover invisibly.
guardrailViolation
What it means: Content was flagged by safety guardrails — OR model assets are missing and the failure is misreported as a guardrail. Inspect debugDescription.
Recoverable: Depends. Asset failures need onboarding/availability handling; genuine rejections need a fallback.
Action: Classify, then route. See the guardrail guide.
User copy: For genuine rejections, silently fall back. Never say "your request was unsafe."
assetsUnavailable
What it means: Required model assets aren't available — model not downloaded, mid-download, or evicted.
Recoverable: Not immediately. The model needs to become available.
Action: Route to your availability-gated UI. Check SystemLanguageModel.default.availability for the specific reason (device not eligible, model not ready, AI features off).
User copy: "On-device intelligence is still setting up" — or hide the feature entirely.
unsupportedGuide
What it means: A @Guide constraint you used isn't supported (e.g. a regex or constraint type the model can't honor).
Recoverable: Only by fixing your schema — this is a developer error, not a runtime condition.
Action: Simplify the offending @Guide. Catch in development; it should never reach production.
User copy: None — fix it before shipping.
unsupportedLanguageOrLocale
What it means: The prompt's language/locale isn't supported by the on-device model. Recoverable: No, for that input. Action: Detect unsupported locales up front and fall back to a non-AI path or a server model where you have one. User copy: Degrade silently; don't expose locale limits.
decodingFailure
What it means: The model produced output that couldn't be decoded into your @Generable type.
Recoverable: Often, with a retry — generation is stochastic.
Action: Retry once with the same or a slightly simplified schema. If it persists, your @Generable type is likely too complex; flatten it.
User copy: None — retry transparently, then fall back.
rateLimited
What it means: Too many requests in too short a window. Recoverable: Yes, with backoff. Action: Exponential backoff and retry. Also a signal to debounce user-driven generation (e.g. don't fire on every keystroke). User copy: None — back off silently.
A complete handler
Exhaustive switching beats a catch-all. This routes every case to the right behavior:
func generate(_ prompt: String) async -> GenerationOutcome {
do {
let answer = try await session.respond(to: prompt)
return .success(answer.content)
} catch let error as LanguageModelSession.GenerationError {
switch error {
case .exceededContextWindowSize:
await recoverContextWindow() // new session, then caller retries
return .retryable
case .guardrailViolation(let ctx):
return classifyGuardrail(ctx) // .assetIssue or .safetyFallback
case .assetsUnavailable:
return .modelUnavailable
case .decodingFailure:
return .retryable // single retry upstream
case .rateLimited:
return .backoff
case .unsupportedLanguageOrLocale,
.unsupportedGuide:
return .fallback // deterministic path
@unknown default:
// Future-proofing: Apple has added cases across point releases.
return .fallback
}
} catch {
return .fallback
}
}
The @unknown default is not optional hygiene here — Apple has added new GenerationError cases across iOS 26 point releases. Without it, a future case silently slips through your exhaustive switch as an unhandled crash-or-misroute. With it, anything unrecognized degrades to your deterministic fallback.
Decoding "error N" messages
The bare GenerationError error 4 comes from a generic localizedDescription on an error you didn't pattern-match. The fix is simply to always switch on the typed enum rather than printing localizedDescription. Once you match the cases above, you never see a numeric code again — you see exceededContextWindowSize, guardrailViolation, and so on, which are actionable.
If you must log the raw error for telemetry, log the case name and context.debugDescription, not the localized string:
private func log(_ error: LanguageModelSession.GenerationError) {
// Stable, greppable telemetry — not the user-facing localized string.
logger.error("GenerationError: \(String(describing: error))")
}
Production checklist
- Switch on the typed enum, never print
localizedDescriptionto the user. - Include
@unknown default— new cases ship in point releases. - Map each case to one of: recover, retry, backoff, fallback, model-unavailable.
- Retryable cases (
decodingFailure,rateLimited) get one retry / backoff, not a loop. - Log the case name +
debugDescriptionfor telemetry; never the numeric code.
Why this matters for shipped apps
The difference between a robust AI feature and a fragile one is entirely in this switch statement. Most tutorials show the happy path and a single catch { }. Production traffic exercises every branch — long sessions, unsupported locales, devices mid-download, the occasional decode miss. Handle them exhaustively once and the feature simply works for everyone; handle them lazily and you ship "error 4" to real users.
Want this error-handling layer designed correctly before launch? It's part of every on-device AI integration we do at 3NSOFTS.