Migrating Foundation Models Error Handling to Xcode 27
- Author
- Ehsan Azish · 3NSOFTS
- Updated
- September 2026
- Read time
- 10 min read
- Level
- Intermediate
- Platform
- Xcode 27 beta; Foundation Models on iOS 26/27 or macOS 26/27
Implementation Notes
- ~/ What broke: Xcode 27 splits generation failures across new error types, so an older catch block can miss the recovery path.
- ~/ What to do: Map the new errors to explicit recovery decisions, preserve cancellation, and retain compatibility with supported older systems.
Quick answer
When rebuilding a Foundation Models app with Xcode 27, replace the assumption that every generation failure is a LanguageModelSession.GenerationError. Failures now have several owners: the language model, the system model's assets, the session, and generated-content parsing. Keep task cancellation separate from all of them.
Version scope: This guide targets the Xcode 27 beta SDK available in September 2026. The new error types are available from iOS/macOS 27. Keep an older-system path if your deployment target includes version 26, and recheck the SDK when adopting the final release. The existing GenerationError reference remains the version-26 companion.
Apple's GenerationError documentation describes the transition as dependent on rebuilding with Xcode 27. Do not infer your binary's error behavior from the device OS alone.
Map cases before changing recovery behavior
The installed Xcode 27 SDK provides the following deprecation replacements. Notice the renamed cases and the parsing error outside the three principal enums.
| Old GenerationError case | Replacement |
|---|---|
exceededContextWindowSize | LanguageModelError.contextSizeExceeded |
assetsUnavailable | SystemLanguageModel.Error.assetsUnavailable |
guardrailViolation | LanguageModelError.guardrailViolation |
unsupportedGuide | LanguageModelError.unsupportedGenerationGuide |
unsupportedLanguageOrLocale | LanguageModelError.unsupportedLanguageOrLocale |
decodingFailure | GeneratedContent.ParsingError |
rateLimited | LanguageModelError.rateLimited |
concurrentRequests | LanguageModelSession.Error.concurrentRequests |
refusal | LanguageModelError.refusal |
The new SDK also exposes cases such as timeout, unsupportedCapability, and unsupportedTranscriptContent on LanguageModelError, plus transcriptMutationWhileResponding on the session error. An old switch translated mechanically will miss those distinctions.
Do not switch on an NSError numeric code or search localizedDescription for a phrase. Those are poor contracts for recovery decisions. Use typed errors, then translate them into a small set of actions owned by your application.
Use a classifier that does not retry by itself
This example separates recognition from execution. A returned policy is a decision for the feature controller; it does not create tasks, clear drafts, or resend requests. That makes it possible to test error routing without invoking a real model.
import Foundation
import FoundationModels
enum GenerationRecovery: Equatable {
case cancelled
case rebuildContext
case waitForAvailability
case offerRetry
case useNonGenerativePath
case repairSessionOwnership
case inspectSchema
case inspectConfiguration
case unknown
}
@available(iOS 26.0, macOS 26.0, *)
func generationRecovery(for error: any Error) -> GenerationRecovery {
if error is CancellationError { return .cancelled }
if #available(iOS 27.0, macOS 27.0, *) {
if let modelError = error as? LanguageModelError {
switch modelError {
case .contextSizeExceeded:
return .rebuildContext
case .rateLimited, .timeout:
return .offerRetry
case .guardrailViolation, .refusal,
.unsupportedLanguageOrLocale:
return .useNonGenerativePath
case .unsupportedGenerationGuide:
return .inspectSchema
case .unsupportedCapability, .unsupportedTranscriptContent:
return .inspectConfiguration
@unknown default:
return .unknown
}
}
if let assetError = error as? SystemLanguageModel.Error {
switch assetError {
case .assetsUnavailable:
return .waitForAvailability
@unknown default:
return .unknown
}
}
if let sessionError = error as? LanguageModelSession.Error {
switch sessionError {
case .concurrentRequests, .transcriptMutationWhileResponding:
return .repairSessionOwnership
@unknown default:
return .unknown
}
}
if error is GeneratedContent.ParsingError {
return .inspectSchema
}
}
// Compatibility with the version-26 API. This reference is deprecated
// in the version-27 SDK; remove it when dropping the older path.
if let legacyError = error as? LanguageModelSession.GenerationError {
switch legacyError {
case .exceededContextWindowSize:
return .rebuildContext
case .assetsUnavailable:
return .waitForAvailability
case .rateLimited:
return .offerRetry
case .guardrailViolation, .refusal, .unsupportedLanguageOrLocale:
return .useNonGenerativePath
case .concurrentRequests:
return .repairSessionOwnership
case .unsupportedGuide, .decodingFailure:
return .inspectSchema
@unknown default:
return .unknown
}
}
return .unknown
}
This is an application policy, not an Apple-prescribed retry strategy. For example, parsing failure leads to inspection here because automatically repeating a side-effecting workflow could be unsafe. A pure text-generation feature may choose one bounded retry after validating its schema. Make that choice where the operation's consequences are known.
The compatibility branch intentionally handles legacy errors after checking new types. It can still recognize an older error passed by another component; it does not assume the OS version proves which type a caller supplied. Projects that treat deprecation warnings as errors should isolate their compatibility adapter with an explicit, documented warning policy rather than suppress warnings across the app.
Preserve cancellation at the operation boundary
A cancellation is usually a request to stop work. Do not turn it into “try again” or silently restart a generation after someone taps Stop.
import Foundation
import FoundationModels
@available(iOS 26.0, macOS 26.0, *)
func generateText(
prompt: String,
session: LanguageModelSession
) async throws -> String {
try Task.checkCancellation()
let response = try await session.respond(to: prompt)
try Task.checkCancellation()
return response.content
}
The second check prevents this helper from returning a result when its task was cancelled during generation. The caller still needs to verify request identity before publishing to UI: an earlier request must not overwrite a newer draft. A cancellation check alone does not establish which request currently owns the screen.
In a catch block, handle an observed CancellationError and the current task's cancelled state as stop conditions. A framework may throw another error near the same time cancellation occurs. Do not use that race as a reason to keep processing work the user no longer wants.
For task ownership examples, use the LanguageModelSession cancellation guide. Remember that SwiftUI's .task lifecycle cancellation and an unstructured Task stored by your own controller require different ownership decisions.
Turn recovery policies into deliberate product behavior
Rebuild context: Preserve the original user input outside the session. Reconstruct a bounded context containing the facts needed for the task, and explain any loss of conversational history. Never retry the same full transcript indefinitely. The context-window guide explains the recovery concept; use the new error name when implementing it with Xcode 27.
Wait for availability: Reevaluate SystemLanguageModel availability and offer a usable non-AI path while the system is not ready. Rapid retries do not download model assets faster. See availability gating.
Offer retry: Keep the draft. For rate limiting, respect a supplied reset date where available and bound any automatic delay. For a timeout, determine whether a tool already performed an action before resending the workflow. A timeout is not evidence that nothing happened.
Repair session ownership: Serialize requests on a shared session, and avoid mutating its transcript while it is responding. Do not solve accidental overlap by allocating unlimited new sessions. Decide whether the feature queues work, rejects overlapping input, or cancels and awaits the previous operation.
Inspect schema or configuration: Retain enough non-sensitive diagnostic context to reproduce the failure. Record the error category, app build, OS version, and operation identifier. Avoid putting prompts, tool payloads, or parsed user documents into routine logs.
Use a non-generative path: Preserve the customer's original content and offer an alternative action. Do not repeatedly reformulate refused requests to force a model response. A model outcome is also not a factual judgment about the user.
Test the migration at two levels
First test the classifier using constructed errors from the SDK and a plain CancellationError. Verify every policy you rely on and the unknown fallback. Those tests exercise your application code without waiting for rare model failures.
Then test the actual feature against the supported OS and build combinations. Constructed errors do not prove what the framework throws at runtime.
- Run the Xcode 27 build on version 27 and on version 26 if supported.
- Cancel during generation and after a response becomes available; verify no obsolete result is saved.
- Submit two rapid requests and verify the session owner follows its intended queue or replacement policy.
- Exercise unavailable-model behavior through a test double, then check real availability handling on a suitable device.
- Verify an oversized-context path preserves the draft and makes a bounded recovery attempt.
- Test a failing tool separately:
ToolCallErrorand your tool's own error handling are outside this classifier's specialized cases. - Confirm a malformed generated value cannot become a valid saved record merely because text generation completed.
Keep the old reference linked for version-26 readers, but make the build and OS scope visible wherever you show a catch block. The migration is complete when the feature preserves work and chooses the right recovery action—not merely when deprecation warnings disappear.