Operations

Error codes

Every code Sirannon can raise, grouped by the part that raises it, with what each one means, which ones are safe to retry, and how each maps to an HTTP status.

Every Sirannon error extends SirannonError and includes a machine-readable code. Match on the code. The message explains what happened to a human reading a log and changes between releases, so treat it as prose. catch-errors.ts inserts an order with no total, which the column's NOT NULL constraint refuses.

mkdir -p data
catch-errors.ts
import { QueryError, Sirannon } from '@delali/sirannon-db'
import { betterSqlite3 } from '@delali/sirannon-db/driver/better-sqlite3'
 
const driver = betterSqlite3()
const sirannon = new Sirannon({ driver })
 
const db = await sirannon.open('shop', './data/shop.db')
 
await db.execute('CREATE TABLE IF NOT EXISTS orders (id INTEGER PRIMARY KEY, total REAL NOT NULL)')
 
try {
  await db.execute('INSERT INTO orders (id) VALUES (?)', [1])
} catch (err) {
  if (err instanceof QueryError) {
    console.error(`SQL failed [${err.code}]: ${err.message}`, err.sql)
  }
}
 
await sirannon.close('shop')
SQL failed [QUERY_ERROR]: NOT NULL constraint failed: orders.total INSERT INTO orders (id) VALUES (?)

Some errors add context alongside the code: sql on QUERY_ERROR, table, rowId, and cause on CONFLICT_ERROR, version on a migration error, limit and retryAfterMs on WRITE_OVERLOADED, requestId on SYNC_ERROR, and serverVersion on MIGRATION_REQUIRED and SCHEMA_AHEAD.

Over the network, an HTTP response and a WebSocket error message use the same shape:

{ "error": { "code": "ERROR_CODE", "message": "Human-readable description", "details": {} } }

A dash in a class column below means Sirannon raises the base SirannonError with that code, so match on err.code for those.

Core engine

ClassCodeWhen
DatabaseNotFoundErrorDATABASE_NOT_FOUNDThe database ID is not registered and cannot be resolved.
DatabaseAlreadyExistsErrorDATABASE_ALREADY_EXISTSA caller registered an ID already in use.
-DATABASE_CLOSEDAn operation ran against a closed database.
-DATABASE_OPEN_FAILEDA database failed to open.
ReadOnlyErrorREAD_ONLYA write, or a live query, ran against a read-only database.
QueryErrorQUERY_ERRORSQLite failed to prepare or execute a statement, on this node or, for a forwarded write, on the primary.
ForbiddenSqlErrorFORBIDDEN_SQLA statement reached a _sirannon table, modified the sqlite_ catalogue, or used ATTACH, DETACH, or PRAGMA writable_schema.
TransactionErrorTRANSACTION_ERROREvery statement in a transaction succeeded and SQLite then refused the commit, as it does for a deferred foreign key that still fails. The error's cause holds SQLite's own error.
HookDeniedErrorHOOK_DENIEDA before-hook rejected the operation.
RequestDeniedErrorthe code you supplyThe authenticate hook refused the request with a status and code of its own.
CDCErrorCDC_ERRORThe change-data-capture pipeline failed, or a statement cannot back a live query.
BackupErrorBACKUP_ERRORA backup failed for a reason none of the codes below name, such as a refused path or a piece size a run could not use.
-BACKUP_UNSUPPORTEDThe driver provides no backup engine, the connection offers no stepped copy call, the node driver runs on a Node.js build whose node:sqlite exports no backup function, or a database asked for backups without a file in write-ahead logging mode.
-BACKUP_RESTARTEDSQLite returned the copy to page one more times than restartLimit allows, or the copy reached no new page across noProgressStepLimit steps.
-BACKUP_STALLEDThe copy completed no step inside stallTimeoutMs.
-BACKUP_ABORTEDA whole write deadline passed with no step of a copy on the writer worker, so the host cancelled it and the worker stopped it part-way.
-BACKUP_DESTINATION_ERRORYour destination refused a call, failed to answer inside destinationTimeoutMs, or holds pieces that do not match the run that wrote them.
-BACKUP_LOG_REWOUNDThe write-ahead log restarted before a capture read it, so the writes those frames held are in no backup.
-BACKUP_CHAIN_BROKENNo full copy goes back as far as the moment asked for, a chain is missing a piece a restore needs, or no chain record states the name given to verifyBackup.
ConnectionPoolErrorCONNECTION_POOL_ERRORThe pool is closed, exhausted, or misconfigured.
MaxDatabasesErrorMAX_DATABASESOpening a database would pass the configured cap.
ExtensionErrorEXTENSION_ERRORA native SQLite extension could not be loaded.
-INVALID_DRIVERDriver configuration failed validation.
-INVALID_SYNCHRONOUSA caller supplied an unknown synchronous level.
-INVALID_DURABILITYA load passed a durability other than 'off' or 'normal'.
-DURABILITY_RESTORE_FAILEDThe load committed, then the writer failed before it restored durability.
-SNAPSHOT_IN_PROGRESSA read or write ran while a device-sync snapshot load was replacing the database.
-SHUTDOWNAn operation ran after registry shutdown.
-SHUTDOWN_ERROROne or more databases failed to close during shutdown.
-LIFECYCLE_DISPOSEDA resolve ran after the lifecycle manager was disposed.
-INTERNAL_SCHEMA_ERRORAn internal-table identifier, column type, or default failed validation, or a schema version fell outside PRAGMA user_version.

Writer worker

The writer worker separates a rejected write from one whose outcome is unknown, which is the difference between retrying safely and reconciling first.

CodeWhenRetry?
WRITE_OVERLOADEDMore writes were pending than maxPendingWrites allows, or the worker shed a queued write when an earlier deadline expired. HTTP returns 503 with Retry-After.Yes. The write never applied.
WRITER_WORKER_TIMEOUTThe writer gave no outcome within twice writeTimeoutMs.Only after reconciling. The outcome is indeterminate.
WRITER_WORKER_EXITThe writer crashed or exited, and every write in flight failed.Only after reconciling. A write in flight may have committed before the crash.
WRITER_WORKER_FATALThe writer passed its restart budget, so writes now fail permanently.No. Restart the process.
WRITER_WORKER_UNAVAILABLEA write arrived while no writer was available.Yes. The write never reached the writer.
WRITER_WORKER_CLOSEDA write arrived after the writer closed, or the writer closed while it was in flight.Reconcile first when the write was already in flight.
WRITER_WORKER_POST_FAILEDThe host could not hand the operation to the writer.Yes. The write never reached the writer.
WRITER_WORKER_NO_PORTThe writer entry point started outside a worker thread.No. Fix the configuration.
WRITER_WORKER_UNSUPPORTEDwriterWorker was enabled on a driver with no worker entry, so the database refuses to open.No. Change the driver or the option.
INVALID_WRITER_WORKERA writerWorker value is out of range.No. Fix the configuration.

Backups

A backup writes to storage you own, so whether a failure is worth retrying depends on what that failure left behind. Sirannon removes a partial local file itself, and a destination that follows the contract lets a later run replace any piece an earlier one stored.

CodeRetry?
BACKUP_RESTARTEDYes, once the connection writing to the source file has stopped. The copy stored nothing usable and Sirannon removed the file it was writing.
BACKUP_STALLEDYes. Sirannon removes the partial file once SQLite stops writing it, so the next run starts clean.
BACKUP_ABORTEDYes. Sirannon removed the partial file, so the next run starts clean. Raise the worker's writeTimeoutMs where a large copy trips it repeatedly.
BACKUP_DESTINATION_ERRORYes, once your storage answers again. A cycle keeps the capture staged and offers it on a later turn by itself.
BACKUP_LOG_REWOUNDNo. The writes those frames held reach no backup, and the cycle starts a fresh chain with a full copy.
BACKUP_CHAIN_BROKENNo. Ask for a moment an intact chain reaches, or let the cycle start a fresh one.
BACKUP_UNSUPPORTEDNo. Change the driver, the runtime, or the database's options.
BACKUP_RESTORE_IN_PROGRESSYes, once the running restore finishes.
BACKUP_RESTORE_NOT_ACCEPTEDNo. The operator has to open the route with acceptBackupRestore.

Migrations

CodeWhen
MIGRATION_ERRORA migration step failed while running.
MIGRATION_VALIDATION_ERRORA migration definition failed validation.
MIGRATION_DUPLICATE_VERSIONTwo migrations share a version.
MIGRATION_NO_DOWNA rollback named a migration with no down statement.
MIGRATION_SOURCE_INVALIDThe registry migration source returned something other than a list.
MIGRATION_CHECKSUM_MISMATCHAn applied migration's stored checksum no longer matches its SQL.
MIGRATION_BASELINE_GAPA history below a baseline lacks the bridging migrations.
MIGRATION_CONCURRENTTwo migration runs overlapped and could not be resolved.
MIGRATION_ROLLBACK_ERRORA rollback step failed.

Server and requests

CodeWhen
INVALID_REQUESTThe request body structure is invalid.
INVALID_JSONThe body or WebSocket message is not valid JSON.
EMPTY_BODYThe request body is empty.
PAYLOAD_TOO_LARGEThe body or message passed maxBodyBytes.
INTERNAL_ERRORSomething unexpected failed while handling the request.
HOOK_ERRORThe authenticate hook or authorizeClusterStatus threw, or authenticate returned a refusal object in place of an identity.
NOT_FOUNDThe route does not exist, or cluster status is absent or refused.
INVALID_MAX_BODY_BYTESmaxBodyBytes is not a positive integer the transport can enforce exactly.
INVALID_WS_BACKPRESSUREmaxWebSocketBackpressureBytes failed validation or fell below maxBodyBytes.
INVALID_BACKUP_RESTOREacceptBackupRestore is true and the server has no authenticate hook, so it refuses to start.
INVALID_DEVICE_SYNCacceptDeviceSync is true and the server has no authenticate hook, so it refuses to start.
BACKUP_RESTORE_NOT_ACCEPTEDA caller reached POST /db/:id/backup/restore on a server that leaves acceptBackupRestore off.
BACKUP_RESTORE_IN_PROGRESSA restore of that database is already under way and keeps its file to itself.
BULK_LOAD_UNSUPPORTEDThe execution target provides no bulk load.
INVALID_MESSAGEA WebSocket message lacks a required field or holds a value of the wrong type.
UNSUPPORTED_SUBPROTOCOLA WebSocket upgrade offered subprotocols, none of them sirannon.v1.
UNKNOWN_TYPEA WebSocket message names an unrecognised type.
HANDLER_CLOSEDThe WebSocket handler is shutting down.
DUPLICATE_SUBSCRIPTIONA subscription with the same ID already exists on the connection.
SUBSCRIPTION_NOT_FOUNDAn unsubscribe named a subscription that does not exist.
CDC_UNSUPPORTEDSubscriptions need a file-based database, and this one is in memory.

Registered operations

Every code here comes from registered operations, and each one names a specific mistake in the request.

CodeWhen
UNKNOWN_QUERYNo operation of that name is registered for the database.
MISSING_ARGUMENTA declared argument was absent from the request.
ARGUMENT_NOT_ALLOWEDThe caller supplied an undeclared argument, or one the server fills from identity.
IDENTITY_REQUIREDAn operation fills an argument from identity and the request supplies none.
REGISTRY_MISMATCHA live query echoed a registry digest this server does not serve.
SQL_NOT_ACCEPTEDThe server accepts no SQL over the network, or it refuses a subscription to a table's changes because it runs without acceptSql and its registry has no onBeforeSubscribe hook.

Replication

Every class here is exported from @delali/sirannon-db/replication, and FailoverError is the shared base of NoSafePrimaryError and UnsafeRecoveryRequiredError.

ClassCodeWhen
ReplicationErrorREPLICATION_ERRORBase class for replication failures.
SyncErrorSYNC_ERRORFirst sync failed: the node was not ready, the transfer timed out, or a manifest or batch order did not match.
ConflictErrorCONFLICT_ERRORA conflict resolver threw while Sirannon applied a change. table and rowId name the row, and cause holds what the resolver threw.
TransportErrorTRANSPORT_ERRORA peer was unreachable or a send failed. A primary that refuses a forwarded write with a Sirannon code, such as QUERY_ERROR for a statement that SQLite rejects, sends that code back to the replica unchanged.
BatchValidationErrorBATCH_VALIDATION_ERRORA batch failed its checksum, broke the schema allowlist, passed maxClockDriftMs, or included unsafe DDL.
WriteConcernErrorWRITE_CONCERN_ERRORThe group did not meet the write concern inside the timeout.
ReadConcernErrorREAD_CONCERN_ERRORThe requested read concern cannot be satisfied.
TopologyErrorTOPOLOGY_ERRORA write reached a replica without forwarding, no primary was available, or a peer was unauthorised.
CoordinatorErrorCOORDINATOR_UNAVAILABLEThe coordinator cannot be reached or cannot prove quorum authority.
AuthorityErrorAUTHORITY_LOSTA node lost primary or controller authority while handling work.
StalePrimaryErrorSTALE_PRIMARYA request, batch, sync message, or forwarded write used a stale primary term.
NoSafePrimaryErrorNO_SAFE_PRIMARYNo eligible in-sync replica can be promoted safely.
NodeNotInSyncErrorNODE_NOT_IN_SYNCThe node is alive but outside the group's in-sync set.
NodeDrainingErrorNODE_DRAININGThe node is in maintenance drain mode.
ProtocolVersionMismatchErrorPROTOCOL_VERSION_MISMATCHNode compatibility metadata is incompatible with the cluster.
UnsafeRecoveryRequiredErrorUNSAFE_RECOVERY_REQUIREDAutomatic recovery needs explicit operator action.

Device sync

CodeWhen
MIGRATION_REQUIREDThe device schema version is behind the server, so the device migrates before it syncs.
SCHEMA_AHEADThe device schema version is ahead of the server, so the server migrates first.
SYNC_UNSUPPORTEDThe execution target applies no changes, or the server predates device sync.
SNAPSHOT_UNSUPPORTEDA snapshot was requested for an in-memory database.
SNAPSHOT_CHECKSUM_MISMATCHA downloaded snapshot page failed checksum verification.
DEVICE_SYNC_NOT_ACCEPTEDThe server leaves acceptDeviceSync off, so it serves no device sync. SyncController.start() raises the same code against such a server.
DEVICE_CLOCK_AHEADA pushed change carried a stamp more than 300,000 milliseconds ahead of the server clock. The device keeps those writes and pushes them again later.
DEVICE_NOT_SUBSCRIBEDAn acknowledgement named a device for which the connection holds no subscription.

Optional packages

Sirannon raises these codes in your own process when it starts a server, a replication transport, or a coordinator whose package it cannot load. The message names the missing package and the command that installs it.

CodeWhen
SERVER_DEPENDENCY_MISSINGserver.listen() could not load uWebSockets.js.
TRANSPORT_DEPENDENCY_MISSINGThe gRPC transport could not load @grpc/grpc-js, @bufbuild/protobuf, or grpc-health-check as the engine started.
COORDINATOR_DEPENDENCY_MISSINGThe etcd coordinator could not load etcd3 when the engine first called it.

Client

CodeWhen
CONNECTION_ERRORThe client failed to connect to the server.
UNAUTHORIZEDThe server refused the WebSocket upgrade as unauthenticated and closed with 4401.
FORBIDDENThe server refused the WebSocket upgrade as not permitted and closed with 4403.
TIMEOUTA request passed the configured requestTimeout.
TRANSPORT_ERRORThe current transport does not support this operation, such as a live query over HTTP.
INVALID_RESPONSEThe server returned a response the client could not parse.
ROUTING_ERRORTopology routing discovered no usable primary or read endpoint.
NO_SAFE_PRIMARYTopology routing in coordinator mode found no current primary for a write or a linearizable read.
INVALID_ARGUMENTA client argument failed validation, such as a per-call read concern on the topology transport.
UNKNOWN_ERRORAn error response named no recognisable code.

Deciding whether to retry

Three groups behave differently, and treating them alike is how a retry loop corrupts data.

  • Definite rejections are safe to send again. WRITE_OVERLOADED reports load shedding, because the write never ran, the response includes Retry-After, and the same request is safe to repeat. WRITER_WORKER_UNAVAILABLE and WRITER_WORKER_POST_FAILED report the same about a write that never reached the writer.
  • Indeterminate outcomes need reconciling first. WRITER_WORKER_TIMEOUT and WRITER_WORKER_EXIT leave the outcome unknown, so read the state back before resending anything that is not idempotent.
  • Stale routing needs a refresh, not a retry. STALE_PRIMARY, AUTHORITY_LOST, COORDINATOR_UNAVAILABLE, NO_SAFE_PRIMARY, and CONNECTION_ERROR mean the client's view of the cluster is out of date. The topology client refreshes its routing metadata on all five in coordinator mode and runs a failed read once more, while it raises a failed write for you to reconcile.

A validation code such as INVALID_WRITER_WORKER, INVALID_MAX_BODY_BYTES, INVALID_DRIVER, or INVALID_ARGUMENT reports a configuration or call-site mistake. Fix the configuration; retrying reproduces it.

UNAUTHORIZED and FORBIDDEN report a refused WebSocket upgrade, and the server refuses that credential every time. The client leaves the connection closed, so issue a fresh credential and build a new client.

HTTP status codes

StatusCodes
400INVALID_REQUEST, INVALID_JSON, EMPTY_BODY, QUERY_ERROR, TRANSACTION_ERROR, INVALID_DURABILITY, INVALID_SYNCHRONOUS, BATCH_VALIDATION_ERROR, MISSING_ARGUMENT, ARGUMENT_NOT_ALLOWED, UNSUPPORTED_SUBPROTOCOL, DEVICE_CLOCK_AHEAD
401IDENTITY_REQUIRED
403READ_ONLY, FORBIDDEN_SQL, HOOK_DENIED, SQL_NOT_ACCEPTED, BACKUP_RESTORE_NOT_ACCEPTED, DEVICE_SYNC_NOT_ACCEPTED
404DATABASE_NOT_FOUND, NOT_FOUND, UNKNOWN_QUERY
409STALE_PRIMARY, PROTOCOL_VERSION_MISMATCH, MIGRATION_REQUIRED, SCHEMA_AHEAD, REGISTRY_MISMATCH, BACKUP_CHAIN_BROKEN, BACKUP_RESTORE_IN_PROGRESS
413PAYLOAD_TOO_LARGE
500INTERNAL_ERROR, HOOK_ERROR, WRITER_WORKER_TIMEOUT
501BULK_LOAD_UNSUPPORTED, SYNC_UNSUPPORTED, BACKUP_UNSUPPORTED
502BACKUP_DESTINATION_ERROR
503DATABASE_CLOSED, SHUTDOWN, READ_CONCERN_ERROR, COORDINATOR_UNAVAILABLE, AUTHORITY_LOST, NO_SAFE_PRIMARY, NODE_NOT_IN_SYNC, NODE_DRAINING, UNSAFE_RECOVERY_REQUIRED, WRITE_OVERLOADED

A code outside the table maps to 500, and a RequestDeniedError uses the status you gave it. WRITER_WORKER_TIMEOUT maps to 500, and not to 503, precisely because its outcome is indeterminate, so no proxy in front of the server treats it as a routine retry.

WebSocket close codes

A status code reaches an HTTP caller, and a close code reaches a socket. No WebSocket client can read the status of a refused handshake, so the server reports those refusals in the close code.

Close codeWhen
1008The database named in the URL is absent or closed.
1013The server is shutting down.
4290The connection's outbound buffer passed maxWebSocketBackpressureBytes, which device sync and subscription resumption both recover from.
4401The authenticate hook refused the upgrade with status 401, and the close reason states your code and message.
4403The hook refused the upgrade with status 403.

The client reconnects after 4290 and resumes each subscription from its cursor. It reads any code from 4000 to 4099 as a refusal and leaves that connection closed, because the server refuses that credential every time.