top of page

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

  1. API operations return domain data or throw (rule 8).
  2. Keep domain data, operation state and retained errors as separate values.
  3. Never swallow business or persistence failures.
  4. Keep cached data visible while a refresh runs or fails.
  5. Storage opening failures follow the request's error path: Feature → ViewModel → feature screen.
  6. 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.

Links

bottom of page