CFA Reference: Errors, Saves and Honest Failure
Formal CFA AI Toolkit for iOS Documentation · Reference 16 of 26
CFA never hides a failure. API operations return domain data or throw; Features keep the latest error as its own value; screens show it with a way to retry.
Definitions
- Domain error: A Swift error that describes a product rule, such as HabitError.invalidName.
- Retained error: A Feature property, such as loadError: String?, kept separately from data and loading state.
- Recoverable request: A request that can fail and then be retried without losing saved data.
- Swallowed failure: A catch block that hides an error from the user; prohibited.
Rules
- API operations return domain data or throw (rule 8).
- Keep domain data, operation state and retained errors as separate values.
- Never swallow business or persistence failures.
- Keep cached data visible while a refresh runs or fails.
- Storage opening failures follow the request's error path: Feature → ViewModel → feature screen.
- Retry the domain request, preserve saved data, and keep independent navigation usable.
Decision rules
- When validation fails, throw a domain error from the Feature.
- When a refresh fails, keep the old data and set the retained error.
- When a save fails, keep the user's draft so they can try again.
- When storage cannot open, show the error on the affected feature's screen and keep Settings reachable.
Example
In the compiled example, HabitsFeature.addHabit(named:) throws for a blank name, and HabitsViewModel.save() keeps the draft and sets its own save error when the Feature throws.
Compiled example (excerpt)
import Foundation
import Observation
// MARK: Screen ViewModel
@MainActor
@Observable
final class HabitsViewModel {
private let habitsFeature: HabitsFeature
@ObservationIgnored private var refreshTask: Task<Void, Never>?
var draftName = ""
private(set) var saveError: String?
init(habitsFeature: HabitsFeature = AppModel.shared.habitsFeature) {
self.habitsFeature = habitsFeature
}
var habitNames: [String] { habitsFeature.habits.map(\.name) }
var isLoading: Bool { habitsFeature.isLoading }
var loadError: String? { habitsFeature.loadError }
/// Repeated presses reuse the one retained task.
@discardableResult
func refresh() -> Task<Void, Never> {
if let refreshTask { return refreshTask }
let habitsFeature = habitsFeature
let newRefreshTask = Task {
await habitsFeature.refresh()
self.refreshTask = nil
}
refreshTask = newRefreshTask
return newRefreshTask
}
func save() async {
do {
try await habitsFeature.addHabit(named: draftName)
draftName = ""
saveError = nil
} catch {
saveError = "The habit could not be saved."
}
}
}Compiled with Swift 6 language mode and warnings treated as errors: type-checked on Swift 6.2 and built on Swift 6.4 (Linux toolchains, Foundation and Observation only).
Common mistakes
- Clearing the list when a refresh fails.
- Clearing the draft before the save has succeeded.
- Using try? on a save.
- Showing a single app-wide error screen for a feature-level failure.
