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
- Follow the message: re-save without `expected` (Save separate copy) so the trace is stored under a new key
- Refresh the trace list and re-open the recording before retrying any update
- Reconcile UI state after the conflict — remove the deleted entry from open editors/lists
- 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
- Always pass expected with the loaded savedAt when updating an existing trace
- Listen for cross-tab deletes (storage events) and drop stale editors
- Refresh the saved list before offering update actions
- Offer 'Save separate copy' as the primary recovery path in conflict UIs
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
- This saved trace changed in another tab. Reopen it before…
- A saved trace identifier collided. Try saving again.
- a window change is already in progress
- Another pet owner is running
- Another pet recorder is using this output.
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)