Engine

Errors

What each error means, whether the write may have happened, and what to do next.

Every error Minnow throws answers three questions: may the operation have happened anyway, is repeating the call sound, and is the connection still usable. classifyError() from @minnowdb/core answers them for any error, including one rehydrated across the worker or the OPFS follower hop and the platform's own QuotaExceededError:

import { classifyError } from "@minnowdb/core";

try {
  await client.insert("orders", order);
} catch (error) {
  const { kind, mayHavePublished, retry, connectionUsable } = classifyError(error);
  if (mayHavePublished)
    await reconcileBySaleId(sale.id); // never blindly resend
  else if (retry === "safe") await backoffAndRetry();
  if (!connectionUsable) await client.reopen();
}

The matrix

KindErrorsMay have publishedRetryConnection usable
unknown-outcomeDatabaseWorkerOutcomeUnknownError, OpfsUncertainOutcomeError — both extend UnknownOutcomeErroryesafter reconcilingsee below
connection-lostDatabaseWorkerTimeoutError, DatabaseWorkerFailedError — both extend ConnectionLostErrornonever on this clientno
conflictWriteConflictError, SchemaConflictError, TableRecordConflictError, TransactionRecordConflictError, lease, compaction, collection, temp-owner, index-build, and snapshot-import conflicts, TableInUseError, OpfsDatabaseInUseErrornoas isyes
rejectedUniqueConstraintError, UniqueKeyConflictError, UniqueIndexCoverageError, MissingKeyError, UnknownTableError, SqlCompileError, CompactionJobCancelledError, any TypeError, RangeError, SyntaxErrornonever as writtenyes
transientOpfsCoordinationError, DatabaseReadBacklogError, MaintenanceBacklogError, CompactionBacklogError, LiveQueryLimitError, LeaseExpiredError, TransactionExpiredError, IndexedDbSchemaUpgradeBlockedError, VisibleSegmentCursorStaleErrornowith backoffyes
resourceQuotaExceededError, StorageResourceLimitError, BlockReadBatchTooLargeError, CompactionMemoryBudgetError, CompactionWriteAmplificationErrornoafter freeing space (retry: "after-reconcile")yes
corruptionStorageCorruptionError, StorageFormatVersionErrornoneveryes
cancelledAn AbortError from the caller's own signalnoas isyes
otherAnything else — a plain Error, a QueryMemoryBudgetError, a platform error the engine does not classifynonever as writtenyes

An unknown-outcome error is the one case where "the call threw" does not mean "nothing happened". The engine never replays a mutation on its own; reconcile a stable identity (a sale id, a revision) before deciding whether to send it again. Its cause tells you whether the connection survived: a mutation cancelled by its own signal leaves the connection usable, one rejected because the worker fell silent does not.

Losing and reopening a connection

DatabaseWorkerTimeoutError and DatabaseWorkerFailedError are fatal for the client: every later call throws the same error. The client reports the loss once through onConnectionLost, and reopen() puts a fresh worker behind the same client with the same store and options:

const client = new MinnowDatabaseClient(() => new Worker(workerUrl, { type: "module" }), {
  store: { kind: "opfs", name: "shop" },
  onConnectionLost: () => void client.reopen().then(rebuildHandles),
});

Handles from before the loss — write scopes, live sets, cursors, buffered writers — belonged to the old worker and must be recreated. A mutation that was in flight was already reported as an unknown outcome.

Background failures that belong to no call — an uncaught exception in the worker, a failed checkpoint, an election that threw — are not errors on any promise. They arrive through onWorkerError, described under Workers.

On this page