linshenkx/prompt-optimizer · error · StorageError

Failed to perform atomic update after ${maxRetries} attempts

Error message

Failed to perform atomic update after ${maxRetries} attempts

What it means

Terminal StorageError thrown by _performAtomicUpdateWithRetry after all retry attempts of the transactional update failed (it rethrows the lastError if one exists; this message only appears when no lastError was captured). The update path uses Dexie transactions with a lock and retries, degrading to _performSimpleUpdate on certain failures.

Source

Thrown at packages/core/src/services/storage/dexieStorageProvider.ts:239

        // 如果是最后一次尝试或非事务错误,尝试降级到简单更新
        if (attempt === maxRetries) {
          console.warn(`All retries failed; falling back to simple update (${key})`);
          try {
            await this._performSimpleUpdate(key, updateFn);
            console.log(`Fallback update succeeded (${key})`);
            return;
          } catch (fallbackError) {
            console.error(`Fallback update also failed (${key}):`, fallbackError);
            throw lastError; // 抛出原始错误
          }
        }
      }
    }

    if (lastError) {
      throw lastError
    }
    throw new StorageError(`Failed to perform atomic update after ${maxRetries} attempts`, 'write')
  }

  /**
   * 简单更新(降级方案)
   */
  private async _performSimpleUpdate<T>(
    key: string,
    updateFn: (currentValue: T | null) => T
  ): Promise<void> {
    try {
      // 读取当前值
      const currentRecord = await this.db.storage.get(key);
      const currentValue = currentRecord?.value
        ? JSON.parse(currentRecord.value) as T
        : null;

      // 应用更新函数
      const newValue = updateFn(currentValue);

View on GitHub (pinned to 3e677b1d9f)

Solutions

  1. Reduce concurrent writers: serialize updates per key or lower update frequency
  2. Avoid doing async non-Dexie work inside the update function passed to update() (causes PrematureCommitError)
  3. Catch this error and fall back to a non-transactional write, or surface it for a user retry

Example fix

// before
await provider.update('counter', fn);

// after
try { await provider.update('counter', fn); }
catch (e) {
  if (e instanceof StorageError) {
    await provider.setItem('counter', await readAndBump(provider)); // manual fallback
  } else throw e;
}
Defensive patterns

Strategy: retry

Validate before calling

null

Type guard

const isAtomicUpdateExhausted = (e: unknown): e is StorageError =>
  e instanceof StorageError && /after \d+ attempts/.test(String((e as Error).message));

Try / catch

try { await provider.update(key, fn); } catch (e) { if (isAtomicUpdateExhausted(e)) { await sleep(250); await provider.update(key, fn); /* single retry with backoff */ } else throw e; }

Prevention

When it happens

Trigger: Repeated transaction failures — e.g., persistent PrematureCommitError, lock contention where the promise-based lock keeps failing, or IndexedDB instability — exhausting maxRetries in update().

Common situations: High-frequency concurrent updates to the same key (counters, settings) in a multi-tab app; performing Dexie operations inside the transaction that trigger premature commits; browser storage corruption or quota pressure.

Related errors


AI-assisted analysis of linshenkx/prompt-optimizer@3e677b1d9f (2026-08-27). Data as JSON: /api/errors/50bb195283122990. Report an issue: GitHub.