Design
Stoic design
Stoic is a robustness library for Swift apps. It gives an app the guarantees
Effect gives a TypeScript program — resources are always released, failures
are classified, retries are policy rather than loops, time and randomness are
injectable, and work never outlives the thing that started it — without
Effect's wrapper type. Stoic's primitives are ordinary async functions,
structs and actors over Swift's own concurrency runtime.
The name is the design. Stoicism separates what you control from what you do not, and meets the second with composure. Stoic separates expected failures (handled, typed, retried by policy) from defects (reported loudly, never retried), and stays composed under the conditions that break hand-rolled code: cancellation, timeouts, hung dependencies, slow cleanup, and the machine going to sleep in the middle of it all.
This document is the north star. Each primitive has its own contract in
design/; this file holds what is common to all of them. A feature that
cannot state its contract in the form below does not ship.
Who it is for
Apps first: macOS and iOS apps whose complexity lives in long-lived background work — audio, models, sync, system integrations, downloads — and whose failures are hangs, leaks and races rather than 500s. Server Swift benefits from the same primitives, and integrates through ServiceLifecycle, but no decision is made for servers at an app's expense.
Two kinds of author write code against Stoic: people, and coding agents. The
second changes the design more than it seems. Agents are good at satisfying a
compiler and following a worked example; they are bad at global reasoning
about ownership and concurrency, and they reach for escape hatches
(@unchecked Sendable, try?, nonisolated(unsafe)) when stuck. So Stoic
puts its safety where a compiler or a build plugin can check it, keeps its
API shaped like plain Swift, and ships its own guidance (design/14).
Principles
Direct style. No effect type, no monad, no combinator chains over wrapped values. An operation is a closure —
() async throws(E) -> T— and the closure is the lazy, re-runnable description Effect reifies. Stack traces, the debugger and Instruments keep working.Every wait has an end. No primitive waits without a bound it can name: a deadline, a budget, a capacity, or the cancellation of its caller. Unbounded queues, retries and joins do not exist in the API.
Work belongs to a scope. Every task Stoic starts is owned by a scope, a slot, a supervisor or a state, and is cancelled and awaited when its owner ends. Stoic never starts work it cannot account for. The one sanctioned exception — abandoning an operation that ignores cancellation — is bounded, counted and reported (design/01).
Cleanup always runs, and runs bounded. Finalizers run on success, failure and cancellation, in reverse order, shielded from cancellation, each under its own deadline.
Failures are classified, not flattened. Stoic passes your error type through untouched. It distinguishes failures (expected, handled) from defects (bugs) and from cancellation (never an error to retry or report).
Time and randomness are injected. Every primitive reads its clock and its random source from the ambient context, so a test replaces both and a thirty-second backoff runs in microseconds, the same way every time.
Observable by default. Every primitive reports what it decides — retrying, giving up, opening, restarting, abandoning — as structured events. A silent decision is a bug.
The compiler first, the build plugin second, the docs third. A guarantee the type system can carry is carried there (noncopyable scope handles, typestate, exhaustive transition tables). What it cannot carry, the lint plugin checks. What neither can, the docs say plainly.
Align with the standard library, then step aside. Where Swift Evolution has an accepted design (SE-0526 deadlines), Stoic matches its names and semantics, and forwards to the standard library once it ships. Stoic's lasting value is what the standard library is unlikely to absorb: budgets, schedules, breakers, supervisors, scopes, schemas, test tooling.
Small core, opt-in edges. The core depends on the standard library,
Synchronization, andosfor its default log sink only. Macros, lint, logging, metrics, tracing and ServiceLifecycle are separate products or traits.
Architecture
┌───────────────────────────────────────────┐
apps import ───────▶ │ Stoic (core: stdlib + Synchronization) │
│ Ambient · Deadline · Schedule · Retry │
│ Concurrency · Scope · Supervisor · Graph │
│ Policies · Report · StateMachine · Schema│
│ runtime · Instrumentation │
└───────────────────────────────────────────┘
▲ ▲ ▲ ▲
┌──────┴──────┐ ┌───────┴───────┐ ┌───────┴───────┐ ┌───────┴────────┐
│ StoicMacros │ │ StoicTesting │ │ StoicLint │ │ Integrations │
│ @Schema │ │ TestClock │ │ build plugin │ │ (traits) │
│ @StateMach. │ │ faults, seeds │ │ + doctor │ │ OSLog, swift- │
│ @Services │ │ model tests │ │ command │ │ log/metrics/ │
│ (swift- │ │ │ │ (swift-syntax)│ │ tracing, SLC, │
│ syntax) │ │ │ │ │ │ AppKit/UIKit │
└─────────────┘ └───────────────┘ └───────────────┘ └────────────────┘| Product | Contents | Dependencies |
|---|---|---|
Stoic |
Every runtime primitive | stdlib, Synchronization |
StoicTesting |
Virtual clock, seeded randomness, fault injection, model tests | Stoic, Swift Testing |
StoicHTTP |
Resilient HTTP client: per-host retry, breaker and bulkhead, idempotency keys, bounded and schema-decoded responses, coalescing; scripted transport for tests | Stoic, StoicSchema, StoicApple, Foundation |
StoicMacros |
@Schema, @Refined (built, behind the Macros trait); @StateMachine, @Services planned |
StoicSchema, swift-syntax |
StoicLint |
Build-tool plugin that fails the build on unsafe patterns | swift-syntax (host tool only) |
| Traits | OSLog, Logging, Metrics, Tracing, ServiceLifecycle, AppKit, UIKit |
the named package, when enabled |
Platform floor: macOS, iOS, tvOS, watchOS and visionOS 27, Swift 6.4,
Swift 6 language mode. The floor is what makes the core honest:
withTaskCancellationShield (SE-0504) and await in defer (SE-0493) are
runtime features of the 27 releases, and Stoic's cleanup guarantee rests on
them. Linux and Windows are not targets for 1.0 (see Open questions).
There is no type named Stoic. A type that shares the module's name shadows
it; callers who need to disambiguate write Stoic::retry (module selectors,
Swift 6.3).
Shared contracts
Every primitive's contract in design/ is written against these.
The ambient context
Clock, randomness and instrumentation are cross-cutting: threading them through every signature is noise, and missing one breaks determinism. They live in one task-local value.
public struct Ambient: Sendable {
public var clock: AnyClock // default: ContinuousClock
public var livenessClock: AnyClock // default: SuspendingClock
public var random: RandomSource // default: system generator
public var instrumentation: any Instrumentation // default: OSLog sink
public static var current: Ambient { get }
}
public nonisolated(nonsending) func withAmbient<R, E: Error>(
_ transform: (inout Ambient) -> Void,
operation: nonisolated(nonsending) () async throws(E) -> R
) async throws(E) -> R- Time-based primitives accept an explicit
clock:that overrides the ambient clock for that call. Randomness is always ambient: one seed for a whole test is what makes a run reproducible. - Two clocks, on purpose. Operation deadlines and retry delays use a continuous clock: if the Mac sleeps through a deadline, the deadline has passed. Liveness checks (watchdogs, heartbeats) use a suspending clock: a process that was asleep was not hung. A watchdog on the wrong clock turns every wake from sleep into a false hang, and a false hang that restarts the app turns into a crash loop.
- Deterministic randomness under concurrency. A shared seeded generator
is not deterministic once two tasks draw from it in an order the scheduler
picks. Stoic derives a stream per call site (a stable FNV hash of seed,
file and line, never
Hasher), so a seed reproduces the same jitter regardless of interleaving. The cost: under one seed, concurrent callers at the same site draw the same jitter. Production uses the system generator, where every draw is independent; a test that studies herd behaviour runs each caller under its own seed. AnyClockerases anyClock<Duration>; its instants are durations from the clock's origin, so a deadline can be stored and compared without generics leaking into every signature. Every sleep is clamped to a century: "wait forever" means "wait for cancellation", never a trap.- Retry budgets live in the ambient context too (
Ambient.budgets), so a test's budgets start full and no other test can drain them.
Errors
Passthrough. A primitive that runs your operation throws your operation's error type, untouched.
retrythat gives up throws the last attempt's error, not aRetryError.Rejections have one type. When a policy refuses to run the operation at all — breaker open, bulkhead full, rate limited, budget exhausted, deadline passed before the start — it throws
PolicyError<Failure>:public enum PolicyError<Failure: Error>: Error { case rejected(Rejection) // the operation never ran (or was abandoned) case failed(Failure) // the operation ran and failed }Policies flatten: a policy wrapping an operation that already throws
PolicyError<F>throwsPolicyError<F>, neverPolicyError<PolicyError<F>>. Pipelines of policies therefore compose without nested unwrapping.Concurrent failures are all kept. Where several children can fail (race, mapConcurrent, scopes with forks), Stoic throws
ConcurrentFailure<Failure>: a non-empty list in completion order, the first being the primary. This is Effect's parallelCause, flattened the way Effect 4 flattened it.Cancellation is not a failure.
CancellationErroris never retried, never counted by a breaker, never reported as an error event, and never wrapped. It propagates as itself.Defects. An error type conforming to
Defectis a programming error: never retried, never handled by a fallback, always reported at error level. Swift traps cannot be caught in-process; the defect protocol is for the thrown kind.
Cancellation
Every primitive's contract has a cancellation clause that answers three questions: what happens to work in flight, whether cleanup runs (always yes), and what is thrown. Stoic never converts cancellation into success, never retries after it, and never sleeps once cancelled.
Isolation
Operation closures are nonisolated(nonsending): they run on the caller's
actor. A @MainActor caller can capture non-Sendable state, exactly as in
the spike that validated this design. Stoic's own shared state lives behind
Mutex (from Synchronization) for synchronous fast paths and actors where
waiting is involved. No Stoic lock is ever held across an await.
Observability
Every decision is an event (design/11). Event names are stable API: a renamed event is a breaking change, so dashboards and log queries built on them keep working across minor versions.
Defaults
Every default is written down, justified and safe: retries are capped and jittered, timeouts exist, breakers need a minimum call count before judging, supervisors escalate. A default that would surprise someone in an incident review is the wrong default.
The primitives
| # | Contract | Replaces | Status |
|---|---|---|---|
| 01 | Time and deadlines | hand-rolled timeout races, asyncAfter timeouts |
Built, including abandoningAfter |
| 02 | Schedules and retry | retry loops, fixed delays, drifting budgets | Built |
| 03 | Structured concurrency | generation counters, semaphores, unbounded streams | Built |
| 04 | Scopes and resources | defer/deinit cleanup, manual teardown order |
Built |
| 05 | Errors and reports | try?, errors flattened to strings |
Built; platform catalog planned |
| 06 | Validation and schema | hand-written validators, ranges repeated three times | Built, with @Schema behind the Macros trait |
| 07 | State machines | phase booleans, stale-callback guards | Built; macro planned |
| 08 | Services and app lifecycle | applicationWillTerminate teardown lists |
Built, with AppKit and UIKit bridges |
| 09 | Supervision | watchdogs, restart-on-failure loops | Built |
| 10 | Resilience policies | nothing between the app and a failing dependency | Built |
| 11 | Observability | silent decisions | Built, with swift-log and swift-metrics adapters |
| 12 | Testing | wall-clock tests, hand-written fakes | Built: task ledger, random walks, deterministic simulation with fault points |
| 13 | Build enforcement | conventions nobody checks | Built: StoicLint plugin with a baseline |
| 14 | Guidance | tribal knowledge | Built: README, AGENTS.md, llms.txt, skill |
| 15 | Persistence | Data.write without fsync, configs corrupted by crashes |
Built: StoicStore |
| 16 | System conditions | ignoring memory pressure, thermal state, background expiry | Built: conditions, DeviceConditions, UIKit expiry, LaunchGuard |
| 17 | Diagnostics | "it hung once, no idea why" | Built: event ring, task ledger, snapshot, flight recorder |
| 18 | Settings | Codable + Data.write, settings lost to crashes and old builds |
Built: StoicSettings |
| 19 | HTTP | hand-rolled retry loops around URLSession, a POST charged twice |
Built: StoicHTTP |
The plan from here. Every domain above is built; what follows is
ordered by how much hand-written fragility each removes from a typical
app, and each lands the way the others did: a contract in design/, a
review, tests for every failure-mode row, small commits.
- Settings that cannot corrupt (
StoicSettings, built: design/18). The composition the pieces were built for: a schema-declared settings value, persisted withAtomicFile, migrated on read, validated on write, observable, with the last good file kept and a corrupt one quarantined. Settings are the most common persistent state in an app and the most commonly lost. - Smarter simulation. Fault points, a PCT/uniform schedule portfolio and early timers are built; next, faults in the simulated file system, learned PCT horizons, and shrinking a failing seed to the fewest preemptions (design/12).
- Macros.
@Schemais built; next,@StateMachine, whose transition table becomes a compile-time-checked declaration with exhaustiveness; and@Services, which checks a service graph's dependencies at compile time (design/06, 07, 08). - Abandonment meets scopes — built. Scopes count abandoned work
started inside them (
scope.abandonedWork); a release declaredifWorkAbandoned: .quarantineis leaked deliberately rather than closed under a call that may still be using it (design/01, 04). - A resilient HTTP layer (
StoicHTTP, built: design/19). Per-host pipelines (timeout, retry honouringRetry-After, breaker, bulkhead), idempotency keys on unsafe methods, responses decoded through schemas with every issue reported, request coalescing, and the outbox for work that must survive a relaunch (the client is built; the outbox composition is not). The network is where most app failures start. - Tracing. swift-distributed-tracing as a trait: retries, deadlines and breaker decisions as span events and attributes.
- Executable failure-mode tables and golden traces — built: a row no
test claims fails the suite (with a shrinking baseline), and
expectTraceasserts the decisions a scenario made (design/11, 12). Next: prove every row in the baseline. - A platform error catalog. Every common
URLError,POSIXError,CocoaErrorandNSOSStatusErrorDomaincode classified for retry, with its meaning in words (design/05).
Quality bar
A primitive ships when, and only when, all of these hold.
- Contract. Its
design/file states semantics, cancellation, errors, isolation, defaults, a failure-mode table ("what happens if…") and the events it emits — and the implementation matches it. - Proof. Deterministic tests on the virtual clock for every row of the failure-mode table. Property-based tests for every piece of arithmetic or state (schedule math, token buckets, breaker windows, transition tables). A stress suite that runs each concurrency test thousands of times.
- Sanitizers. Clean under Thread Sanitizer and Address Sanitizer in CI.
@unchecked Sendableis forbidden in Stoic's own code;unsafeexpressions (strict memory safety) need a written justification. - Warnings are errors. In Stoic's own build, always (Package.swift).
- Performance. Hot paths (breaker admission, semaphore acquire,
Ambient.current, schedule step) have benchmarks with allocation counts, and a regression fails CI. - API discipline. Every public symbol is documented with an example
that compiles;
swift package diagnose-api-breaking-changesgates every release; deprecations get one minor version of overlap. - Used in anger. It has replaced a hand-rolled version in a shipping app and survived a release there.
What apps get wrong today
The same failures recur in every Swift app with long-lived background work. Each primitive exists because one of them is common, costly, and easy to write wrongly by hand.
| The hand-rolled pattern | What goes wrong | Stoic |
|---|---|---|
| A timeout built from a continuation, two tasks and a "first answer" flag | The timer outlives the work; the losing work cannot be cancelled and leaks | withTimeout, abandoning form (01) |
| A retry loop with a fixed count and delay | No jitter, no budget, retries cancellation, and the numbers drift from the docs | Schedule, retry, RetryBudget (02) |
| A generation counter compared in every callback | One forgotten comparison publishes a stale result | TaskSlot, state-scoped tasks (03, 07) |
Cleanup in defer and deinit |
Cannot await, skipped under cancellation, unbounded when the resource hangs |
withScope, Resource (04) |
try? and errors flattened to strings |
The cause, the context and the decision to retry are all lost | Report, Defect (05) |
| Ranges and defaults repeated in the parser, the clamp, the settings UI and the docs | They drift; invalid input is silently clamped instead of reported | @Schema (06) |
| A handful of booleans tracking a lifecycle | Impossible combinations become reachable; work started in one phase leaks into the next | StateMachine (07) |
A teardown list in applicationWillTerminate |
Unordered, unbounded, failures swallowed, and synchronous where cleanup is async | ServiceGraph, lifecycle bridges (08) |
| A timer that pings a thread and aborts | Fires on wake from sleep and during slow launches; restarts become crash loops | Supervisor, Heartbeat (09) |
| Nothing at all between the app and a failing dependency | Every caller hammers it; one slow endpoint exhausts every thread | Circuit breaker, bulkhead (10) |
DispatchSemaphore.wait() and usleep inside async code |
Starves the cooperative pool; deadlocks under load | Lint rules (13) |
| Tests that sleep for real time | Slow and flaky; the interesting interleavings are never reached | TestClock, seeds, fault injection (12) |
Roadmap
| Milestone | Delivers | Status |
|---|---|---|
| M0 | Repository, strict build, design contracts, guidance | Done |
| M1 | Ambient, AnyClock, deadlines, Schedule, retry, recur, budgets; test clocks and seeds |
Done |
| M2 | Scopes, Resource, Saga, TaskScope; TaskSlot, race, mapConcurrent, semaphores, SingleFlight, Channel |
Done |
| M3 | Report, Defect, the event catalog; Secret, IdempotencyKey |
Done |
| M4 | StoicJSON, StoicSchema phase 1 |
Done |
| M4.2 | Schema phase 2: unions, transforms, migrations, generators, SchemaSuite, Codable bridge | Done |
| M4.3 | StoicMacros: @Schema and @Refined, behind the Macros trait |
Done |
| M5 | StateMachine, StateGraph |
Done |
| M6 | ServiceGraph, Supervisor; lifecycle bridges |
Done |
| M7 | Circuit breaker, rate limiter, bulkhead, hedging, pipelines | Done |
| M8 | StoicLint build plugin with a ratchet baseline |
Done |
| M9 | Persistence, system conditions, diagnostics | Done |
| 1.0 | API freeze after every primitive has shipped in a real app; SSWG incubation application | — |
Decisions on record
Apache-2.0, and public. The license of Swift itself and of most of the Swift Server Workgroup's packages: permissive, with a patent grant from every contributor and contributions licensed by submission. A source-available license (as Lodestar uses) would protect an app, but would keep a library out of the companies and the workgroup it is for.
No
Effect<A, E, R>type. Swift has no error unions, no higher-kinded types and no do-notation; result builders cannot express monadic bind. Bow and SwiftEffect died of this, and Point-Free's TCA 2.0 moved from effect values to scoped tasks. Closures are the effect value.No compile-time
Rchannel. Swift cannot express an unordered, subtractable set of requirements. The service graph is validated at compile time by a macro when it is declared in one place, and at startup otherwise (design/08).Scope handles are
~Copyableand borrowed. The compiler rejects any attempt to capture one in an escaping closure, which is the guarantee~Escapablewould give, without the experimental lifetime features that constructing a~Escapablevalue still requires in Swift 6.4. Verified against the 6.4 compiler.Value handles over type-keyed lookup. Services are passed as values, so two databases or two HTTP clients coexist. Only cross-cutting context (clock, randomness, instrumentation, deadline) is ambient.
Cooperative deadlines by default, abandonment by request. Matches SE-0526; the abandoning form exists because some work (CoreAudio, AX calls into a hung app) cannot be cancelled, and real apps depend on it.
The platform floor is 27. Back-deploying the cleanup guarantee means emulating shields with unstructured tasks, and an emulation that is mostly right is worse than a floor.
Open questions
- CI runners. GitHub's hosted runners must offer macOS 27 with Xcode 27
before CI can run Stoic's tests; until then, CI is a self-hosted runner
or local
scripts/check.sh. - Linux. Server users will ask. The core is Foundation-free, so the cost is CI and the Linux story for shields (which have no availability gate there).
- Upstreaming.
retrywith schedules overlaps swift-async-algorithms PR #364. If it merges, Stoic wraps it; Stoic's budgets, deadline awareness and classification are the value on top. Worth a forum post once M1 is real.
Every contract
Each primitive has its own contract: where the idea comes from, the API, what happens when things fail, and how it is tested.
- 01Time and deadlinesEvery wait has an end (Principle 2). This contract defines how Stoic names that end: deadlines that propagate through the task tree, timeouts that are deadlines with a duration, and the one sanctioned way to stop waiting for work that will not stop.
- 02Schedules and retryRetries are policy, not loops. A Schedule is a value that decides, attempt by attempt, whether to go again and how long to wait. retry and recur run an operation under one. Budgets make sure that retrying can never turn an outage into a bigger one.
- 03Structured concurrencySwift's task groups are the right foundation and the wrong altitude. This contract adds the shapes apps keep hand-rolling — "only the latest", "at most n at once", "first good answer", "everyone shares one fetch" — with the cancellation and failure semantics written down.
- 04Scopes and resourcesA scope is Effect's Scope plus acquireRelease, built from the parts Swift 6.4 finally ships. Anything acquired in a scope is released when the scope ends — on success, on failure, on cancellation — in reverse order, shielded from cancellation, and within a bounded time. Work forked in a scope is cancelled and awaited before any release runs.
- 05Errors and reportsFailures are classified, not flattened. An app needs three things from an error: what failed (the original error, intact and matchable), where and why (what the app was doing at each layer), and what to do (retry, give up, tell the user, file a bug). Hand-rolled code loses all three: try? discards the error, "\(error)" flattens it to a string, and a catch that rethrows something new loses the original.
- 06Validation and schemaParse, don't validate. Data that crosses a trust boundary — a config file, a network response, a document from disk, a deep link — enters the app through one declaration per type, and that declaration is the single source of truth for decoding, encoding, defaults, every error message, the JSON Schema, test generators, UI metadata and migrations. Nothing else repeats it, so nothing can drift from it.
- 07State machinesMake illegal states unrepresentable, and make work belong to a state. A lifecycle tracked in a handful of booleans — isOpen, isListening, isClosing, heardSomething — has 2ⁿ combinations, most of them impossible and some of them reachable anyway. Work started in one phase leaks into the next, and every callback compares a generation counter to find out whether it still matters.
- 08Services and app lifecycleEffect's Layer, for apps. Every app has a handful of long-lived services — a database, a network client, a sync engine, a model — that depend on each other, must start in the right order, and must stop in the reverse order when the app quits, the user signs out, or a test ends. By hand, startup is a sequence in applicationDidFinishLaunching and teardown is a list in applicationWillTerminate: unordered when it matters, unbounded when a service hangs, synchronous where cleanup is async, and with every failure swallowed.
- 09SupervisionLong-lived work fails. A sync engine loses its connection, an event tap gets disabled by the system, a model server crashes on a bad input. The hand-rolled answers — a watchdog timer that pings and aborts, a while true { do { … } catch { sleep } } loop — restart too eagerly or not at all, fire during a slow launch or right after the Mac wakes, and turn a persistent fault into a crash loop. Supervision is the discipline for this, and Erlang has had the specification right for thirty years.
- 10Resilience policiesRetries (design/02) decide what to do after a failure. These policies decide whether to call at all: a circuit breaker stops calling a dependency that is failing, a bulkhead stops one slow dependency from consuming every task, a rate limiter keeps a caller inside what a service will accept, and a hedge spends a little extra load to cut the slow tail. Each is a small value you construct once, share, and run operations through; a pipeline composes them without nesting their errors.
- 11ObservabilityA silent decision is a bug. Every time a Stoic primitive decides something the app's author did not explicitly write — retry or give up, drop an element, abandon a finalizer, restart a child, open a circuit — it reports the decision as a structured event. When a user says "it hung for a minute and then worked", the events say which retry slept, which deadline passed, and which child restarted.
- 12TestingA robustness library is only as good as the evidence that its guarantees hold, and the code built on it is only as good as the tests its users can write. Both depend on the same thing: making time, randomness and failure controllable.
- 13Build enforcementA convention nobody checks is a suggestion. Stoic's rules (bound every wait, no escape hatch without a reason, no blocking in async code) live in AGENTS.md and in review comments, and they erode at the speed of a deadline. This contract moves them into the build: the patterns Stoic exists to replace fail the build, with a message that names the replacement.
- 14GuidanceTwo kinds of author write code against Stoic: people, and coding agents. Agents are good at satisfying a compiler and following a worked example; they are weak at global reasoning about ownership and concurrency, and when stuck they reach for escape hatches. Research on agent code quality points the same way: compilers are excellent verifiers (most compile errors in LLM-written TypeScript are type errors, and constraining generation by types cuts them by half or more), but models are measurably weaker in Swift than in Python or JavaScript, and weakest on the newest features — exactly the ones Stoic relies on. So Stoic ships its own knowledge, in the places agents and people look.
- 15PersistenceEvery app writes files it cannot afford to lose — settings, drafts, a queue of work not yet sent — and nearly every app writes them in a way that a crash, a power loss or a full disk can corrupt. data.write(to:) without .atomic leaves half a file; with .atomic, it still calls fsync, which on Apple platforms does not flush the drive's own cache, so a power loss can lose a write the app was told had succeeded. A corrupt settings file then crashes the app on every launch.
- 18SettingsSettings are the most common persistent state in an app and the most commonly lost. The hand-written version is a Codable struct written with Data.write(to:): one crash mid-write and the file is empty; one renamed field and decoding fails and every preference silently resets; one older app version launched after a newer one and the newer fields are dropped on the next save; one invalid value set from code and it is persisted, and the next launch cannot read it. StoicSettings is the composition the schema and store layers were built for.
- 19HTTPMost failures an app shows its user start on the network, and the hand-written client is where every earlier contract gets broken at once: a retry loop that retries a POST twice and charges the card twice; no timeout, or one timeout for the whole call and none per attempt; 429 retried at once instead of after Retry-After; every screen hammering a server that is down; a 2 GB response read into memory; a decoding failure reported as "The data couldn't be read because it isn't in the correct format"; an Authorization header in a log. StoicHTTP is the client a careful team would write, from the pieces Stoic already has.