# Partial sync failures do not corrupt local state

- **Pattern:** `ab-001884` (`mobile-offline-storage.sync-conflicts.partial-sync-safety`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001884
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001884)

## Why it matters

A sync that processes records sequentially without transaction semantics can leave the local database in a partially-updated state if the process is interrupted mid-way. CWE-362 applies when an interrupted sync produces orphaned references — a local record pointing to a remote entity that was never fetched. ISO 25010:2011 reliability.recoverability requires that interrupted operations leave data in a valid, queryable state. The failure mode is subtle: the app appears to function normally after the crash, but relationship integrity is broken and queries silently return incomplete results.

## Severity rationale

Medium because partial sync failures corrupt relational integrity locally, but the exposure is confined to the affected device rather than the server or other users.

## Remediation

Batch sync updates inside a database transaction so the local state either advances fully or not at all. Validate that all remote records can be fetched before writing any of them.

```tsx
async function syncAtomic(records: LocalRecord[]) {
  // Phase 1: fetch everything before writing anything
  const remoteRecords = await Promise.all(
    records.map(r => fetchRemote(r.id))
  );

  // Phase 2: merge and write inside a transaction
  await db.transaction(async (tx) => {
    for (let i = 0; i < records.length; i++) {
      const merged = merge(records[i], remoteRecords[i]);
      await tx.update('records', merged.id, merged);
    }
  });
}
```

For storage layers without native transactions (e.g., AsyncStorage), implement a write-ahead staging key: write the full batch to a staging key first, then atomically swap it into the live key. Never partially update the live key.

## Detection

- **ID:** `partial-sync-safety`
- **Severity:** `medium`
- **What to look for:** Examine sync logic for error handling. Look for transaction semantics, rollback logic, or defensive merging that prevents partially synced data from corrupting the local state.
- **Pass criteria:** Count all sync error handling paths. If sync fails mid-way (e.g., some records sync, others do not), the local state remains consistent. At least 1 transaction boundary, rollback mechanism, or validation pass must exist. Either all changes sync or none do (atomic), or partial syncs leave data in a valid state.
- **Fail criteria:** If sync partially completes, local state could be left inconsistent (e.g., references to records that did not sync). No more than 0 sync operations should lack error handling.
- **Skip (N/A) when:** Never — data consistency is essential.
- **Detail on fail:** `"Sync processes multiple records sequentially with no rollback. If sync fails mid-way, app could have orphaned references or inconsistent relations."`
- **Remediation:** Use transactions for critical sync operations:

  ```tsx
  async function syncRecordsAtomic(records: Record[]) {
    try {
      // Use database transaction
      await db.transaction(async (tx) => {
        for (const record of records) {
          const remote = await fetchRemoteRecord(record.id);
          const merged = mergeConflict(record, remote);
          await tx.update('records', merged.id, merged);
        }
      });

      console.log('Sync completed successfully');
    } catch (error) {
      console.error('Sync failed, all changes rolled back:', error);
      // Local state unchanged
    }
  }

  // For simpler cases without a transaction layer:
  async function syncRecordsWithValidation(records: Record[]) {
    const updated = [];

    try {
      // First pass: validate all records can be fetched
      for (const record of records) {
        const remote = await fetchRemoteRecord(record.id);
        updated.push({ local: record, remote });
      }

      // Second pass: only update if all fetches succeeded
      for (const { local, remote } of updated) {
        const merged = mergeConflict(local, remote);
        await saveLocalRecord(merged);
      }

      console.log('All records synced');
    } catch (error) {
      console.error('Sync validation failed:', error);
      // Local state unchanged
    }
  }
  ```

---

## External references

- cwe CWE-362
- iso-25010 reliability.recoverability

Taxons: data-integrity, error-resilience

HTML version: https://auditbuffet.com/patterns/ab-001884
