> ## Documentation Index
> Fetch the complete documentation index at: https://docs.myme.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors and testing

> Conflict handling, errors, testing, concurrency model

## Conflict handling

See [Errors — version\_conflict](/api/errors#version-conflict) for the enriched 409 payload and [Conflicts](/concepts/conflicts) for the resolution model. The Swift SDK exposes the three modes as `ClientConfiguration.conflictStrategy` and `UpdateOptions.conflict`.

```swift theme={null}
try await client.items.update(
  id: note.id,
  properties: ["body": .string("...")],
  options: UpdateOptions(
    version: note.version,
    conflict: .callback,
    resolve: { conflict in
      return mergeManually(conflict)
    }
  )
)
```

`.auto` (default), `.manual` (throws `ConflictError` with `current`, `ancestor`, `conflictingFields`, `clientPatch`, `mergePolicy`), and `.callback` are the three modes.

`.auto` is policy-aware. The SDK reads the type's `merge_policy` directly from the 409 response and resolves each conflicting field per its strategy. `last_writer_wins` fields take the server's value; `keep_both_copies` fields spawn a sibling item of the same type, tagged `conflicted-copy`, populated from the client's in-flight values for the keep-both fields and the server's `current` values for the rest. The original item retains the server's winning state for the conflicted keep-both fields; non-keep-both client changes apply normally.

The `SyncEngine.events` stream emits `.conflictAutoMerged(payload:)` whenever a replay auto-merge fires:

```swift theme={null}
public struct ConflictAutoMergedPayload: Sendable {
  public let itemId: String           // the original item
  public let mergedItemId: String     // the original item id (post-retry)
  public let conflictedCopyId: String?  // the new sibling, when keep-both spawned one
  public let fields: [String]         // the conflicting fields
  public let strategy: ConflictResolutionOutcome  // .lastWriterWins, .keepBothCopies, .mixed
}
```

Apps typically toast when `conflictedCopyId != nil` — the user's in-flight edit was preserved as a sibling rather than discarded.

In synced mode the chosen `conflict` strategy is captured with the queued mutation so it applies on replay against any 409 the server returns. The `.callback` resolver closure can't be queued (closures aren't `Codable`); on replay, `.callback` degrades to `.auto`.

## Errors

Errors are subclasses of a base `MarfaError`. Pattern-match on the subclass:

```swift theme={null}
do {
  _ = try await client.items.get(id: "bogus")
} catch let error as NotFoundError {
  // missing item
} catch let error as ConflictError {
  // error.current, error.ancestor, error.conflictingFields
} catch let error as LocalModeUnsupportedError {
  // remote-only call (blobs, types, keys, webhooks) on a pure-local client
} catch let error as MarfaError {
  // everything else — error.code (String), error.message, error.status, error.details
}
```

<Info>
  `MarfaError.code` is a `String`, not an enum. Branch on the subclass where one exists; fall through to `MarfaError` and inspect `code` for anything else. Error codes are stable strings (`"version_conflict"`, `"rate_limited"`) — branch on them, not on the message.
</Info>

Subclasses: `NotFoundError`, `ValidationError`, `UnauthorizedError`, `ForbiddenError`, `ConflictError`, `NetworkError`, `ResponseDecodingError`, `LocalModeUnsupportedError`.

## Testing

`MarfaSDKTestSupport` ships as a separate product alongside `MarfaSDK` and provides scaffolding for unit tests:

* `MockTransport` — stub for the `Transport` protocol. Queue responses, assert on captured requests.
* `InMemoryKeychain` — in-memory `SecureStorage` substitute. SPM test binaries run unsigned and can't reach the real Keychain.
* `MarfaSDKTest.makeInMemoryClient()` — pure-local `MarfaClient` backed by an in-memory `ModelContainer`. Drop-in replacement for `MarfaClient.local(path:)` in test setups; sidesteps the cross-test container issues that show up when sharing a file-backed store across cases in the same process.

Add `MarfaSDKTestSupport` as a dependency on your test target only. The product is non-semver-stable across SDK minor versions and is not intended for production code.

The `Transport` protocol is public. `MockTransport` exists primarily for mocking the network in unit tests; the production `MarfaClient` init always instantiates `URLSessionTransport`. The protocol includes `rawUpload(method:path:body:contentType:query:onBytesSent:)` — the progress-aware upload hook that forwards `URLSessionTaskDelegate.didSendBodyData` — so stubs exercising byte-progress code paths (e.g. against `BlobUploadProgressQuery`) can drive those callbacks deterministically from a mock.

## Concurrency model

* `MarfaClient` is `Sendable` — safe to pass across actors.
* Namespace methods are `async throws` and don't require caller isolation.
* `LocalStore`, `MutationQueue`, `SyncEngine`, `ConnectionStateManager` are Swift actors. Shared state is actor-isolated; callers never synchronise manually.
* `MarfaStore` and query classes are `@MainActor @Observable` — observe them directly from SwiftUI views.
* The whole package builds under Swift 6 strict concurrency with zero `@unchecked Sendable` leaks in the public surface.
