Hmbown/CodeWhale · error · Error

This saved trace was deleted in another tab. Use Save…

Error message

This saved trace was deleted in another tab. Use Save separate copy to keep the loaded recording.

What it means

TraceLibrary.save performs optimistic concurrency control on IndexedDB: when saving over an existing trace you must pass `expected` (key + savedAt). If the record with that key is gone at commit time, it was deleted in another tab, and save fails with this message instead of silently recreating it. savedAt acts as the version number; a fresh save (no expected) cannot hit this path.

Solutions

  1. Follow the message: re-save without `expected` (Save separate copy) so the trace is stored under a new key
  2. Refresh the trace list and re-open the recording before retrying any update
  3. Reconcile UI state after the conflict — remove the deleted entry from open editors/lists
  4. Coordinate deletes across tabs (storage event or refresh) if multi-tab editing is common

Example fix

// before: blind update over possibly-deleted record
await library.save(editedTrace, loadedRef); // throws if deleted elsewhere

// after: fall back to a fresh save on conflict
try { await library.save(editedTrace, loadedRef); }
catch (e) { if (String(e.message).includes('deleted in another tab')) await library.save(editedTrace); else throw e; }
Defensive patterns

Strategy: try-catch

Validate before calling

const stillExists = (await library.list()).some(s => s.key === expected.key);
if (!stillExists) console.warn('trace was deleted elsewhere; save as a new copy instead of updating');

Type guard

const isDeletedElsewhere = (e) => e instanceof Error && e.message.includes('deleted in another tab');

Try / catch

try {
  await library.save(trace, expected);
} catch (e) {
  if (isDeletedElsewhere(e)) await library.save(trace); // Save separate copy under a new key
  else throw e;
}

Prevention

When it happens

Trigger: Calling save(trace, expected) where a traces.get(key) inside the readwrite transaction returns undefined — the record was deleted (another tab, another window) between loading it and saving.

Common situations: Two browser tabs open on the same origin, one deleting the trace while the other edits and saves it; a delete in the same app before an async save resolves; stale UI listing a trace that was already removed; storage cleared externally while the page stayed open.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/5879ac14731d4c76. Report an issue: GitHub.

Appendix: source

Thrown at pet/src/ui/storage.ts:81

      catch (e) { this.db = undefined; reject(problem(e as DOMException)); return; }
      let result: T, failure: Error | undefined;
      const fail = (error: Error) => { failure ??= error; try { tx.abort(); } catch { reject(failure); } };
      tx.oncomplete = () => failure ? reject(failure) : resolve(result);
      tx.onerror = () => { failure ??= problem(tx.error); };
      tx.onabort = () => reject(failure ?? problem(tx.error));
      try { action(tx.objectStore('traces'), tx.objectStore('summaries'), value => { result = value; }, fail, tx.objectStore('habitats')); }
      catch (e) { fail(e as Error); }
    });
  }
  async save(trace: Trace, expected?: SavedReference): Promise<SavedReference> {
    await this.open(); // Origin error before secure-context-only randomUUID.
    const key = expected?.key ?? crypto.randomUUID();
    return this.transaction('readwrite', (traces, metas, done, fail) => {
      const req = traces.get(key);
      req.onsuccess = () => {
        try {
          const previous: SavedTrace | undefined = req.result;
          if (expected && !previous) throw missing();
          if (expected && previous!.savedAt !== expected.savedAt) throw changed();
          if (!expected && previous) throw new Error('A saved trace identifier collided. Try saving again.');
          const savedAt = this.nextTime(previous?.savedAt), item: SavedTrace = { key, savedAt, trace };
          traces.put(item); metas.put(summary(item)); done({ key, savedAt });
        } catch (e) { fail(e as Error); }
      };
    });
  }
  private nextTime(previous?: string): string {
    const last = previous ? Date.parse(previous) : 0;
    return new Date(Math.max(Date.now(), Number.isFinite(last) ? last + 1 : 0)).toISOString();
  }
  async list(): Promise<SavedSummary[]> {
    return this.transaction('readonly', (_traces, metas, done) => {
      const req = metas.getAll();
      req.onsuccess = () => done((req.result as SavedSummary[]).sort((a, b) => b.savedAt.localeCompare(a.savedAt)));
    });
  }

View on GitHub (pinned to 433685b202)