Hmbown/CodeWhale · error · Error

This saved trace changed in another tab. Reopen it before…

Error message

This saved trace changed in another tab. Reopen it before updating, or use Save separate copy.

What it means

This is the version-conflict branch of the same optimistic lock: save(trace, expected) compares the stored record's savedAt with expected.savedAt. If another tab modified the trace (advancing savedAt by at least 1 ms) after you loaded it, the save aborts with this message so concurrent edits never silently overwrite each other.

Solutions

  1. Reopen the trace to load its current savedAt and reapply your edits, then save with the fresh expected reference
  2. Use Save separate copy (save without expected) to keep your edits under a new key without clobbering
  3. Diff your edit against the current stored version before re-saving to avoid losing the other tab's changes
  4. Avoid editing the same trace in multiple tabs, or refresh after external saves

Example fix

// before: stale expected reference
await library.save(myEdits, staleRef); // savedAt mismatch -> throws

// after: reload current version, merge, save with fresh expected
const fresh = (await library.list()).find(t => t.key === staleRef.key);
const current = await library.getEntry(fresh.key);
await library.save(merge(current.trace, myEdits), { key: fresh.key, savedAt: fresh.savedAt });
Defensive patterns

Strategy: try-catch

Validate before calling

const current = (await library.list()).find(s => s.key === expected.key);
if (current && current.savedAt !== expected.savedAt) console.warn('trace changed elsewhere; reopen before updating');

Type guard

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

Try / catch

try {
  await library.save(trace, expected);
} catch (e) {
  if (isChangedElsewhere(e)) {
    const fresh = await library.getEntry(expected.key); // reopen to get new savedAt
    await library.save(merge(fresh.trace, trace), { key: expected.key, savedAt: fresh.savedAt });
  } else throw e;
}

Prevention

When it happens

Trigger: Calling save(trace, expected) where traces.get(key) returns a record whose savedAt differs from expected.savedAt — i.e. the record was updated elsewhere between load and save.

Common situations: Two tabs editing the same saved trace simultaneously; one tab auto-saves while the other holds a stale copy; replaying/importing overwrites a trace in another tab; long-lived editor sessions where the stored savedAt moved on.

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/9a7aae498f430d72. Report an issue: GitHub.

Appendix: source

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

      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)));
    });
  }
  async getEntry(key: string): Promise<SavedTrace> {

View on GitHub (pinned to 433685b202)