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.

Table of Contents

Every Sirannon error extends SirannonError and carries 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 rather than an identifier. 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 carry extra context alongside the code: sql on QUERY_ERROR, table and rowId 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 carry 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_EXISTSAn ID already in use was registered again.
-DATABASE_CLOSEDAn operation ran against a closed database.
-DATABASE_OPEN_FAILEDA database could not be opened.
ReadOnlyErrorREAD_ONLYA write, or a live query, ran against a read-only database.
QueryErrorQUERY_ERRORSQLite failed to prepare or execute a statement.
ForbiddenSqlErrorFORBIDDEN_SQLA statement reached a _sirannon table, modified the sqlite_ catalogue, or used ATTACH, DETACH, or PRAGMA writable_schema.
TransactionErrorTRANSACTION_ERRORA transaction could not commit, or it rolled back.
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.
-BACKUP_UNSUPPORTEDThe driver provides no backup engine.
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_SYNCHRONOUSAn unknown synchronous level was supplied.
-INVALID_DURABILITYA load passed a durability other than 'off' or 'normal'.
-DURABILITY_RESTORE_FAILEDThe load committed, then the writer failed before durability was restored.
-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 a queued write was shed 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 was rejected.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.

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 was requested for a migration carrying no down.
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 rather than 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.
BULK_LOAD_UNSUPPORTEDThe execution target provides no bulk load.
INVALID_MESSAGEA WebSocket message lacks a required field or carries a wrong type.
UNSUPPORTED_SUBPROTOCOLA WebSocket upgrade offered subprotocols, none of them sirannon.v1.
UNKNOWN_TYPEA WebSocket message carries 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 names a specific mistake in the request rather than a general failure.

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 carries none.
REGISTRY_MISMATCHA live query echoed a registry digest this server does not serve.
SQL_NOT_ACCEPTEDThe server accepts no SQL over the network.

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_ERRORConflict resolution failed for a table and row.
TransportErrorTRANSPORT_ERRORA peer was unreachable or a send failed.
BatchValidationErrorBATCH_VALIDATION_ERRORA batch failed its checksum, broke the schema allowlist, passed maxClockDriftMs, or carried unsafe DDL.
WriteConcernErrorWRITE_CONCERN_ERRORThe write concern was not met within 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.

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 carry 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 found no current primary for a write.
INVALID_ARGUMENTA client argument failed validation, such as a per-call read concern on the topology transport.
UNKNOWN_ERRORAn error response carried 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 is load shedding: the write never ran, the response carries Retry-After, and the same request is safe to repeat. WRITER_WORKER_UNAVAILABLE and WRITER_WORKER_POST_FAILED say the same thing 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, and raises a failed write rather than resending it.

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
401IDENTITY_REQUIRED
403READ_ONLY, FORBIDDEN_SQL, HOOK_DENIED, SQL_NOT_ACCEPTED
404DATABASE_NOT_FOUND, NOT_FOUND, UNKNOWN_QUERY
409STALE_PRIMARY, PROTOCOL_VERSION_MISMATCH, MIGRATION_REQUIRED, SCHEMA_AHEAD, REGISTRY_MISMATCH
413PAYLOAD_TOO_LARGE
500INTERNAL_ERROR, HOOK_ERROR, WRITER_WORKER_TIMEOUT
501BULK_LOAD_UNSUPPORTED, SYNC_UNSUPPORTED
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 rather than 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 carries 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.