loceldocumented
Reference

Error codes

Every error locel can raise, where it comes from, and what to do about it.

Every refusal in locel has a code. Codes are part of the public API: a code never changes meaning, and a code is never reused for a different condition.

Status: Building. Every code here is declared, and the declaration, boot, write-refusal and attach codes are raised from @locel/core, @locel/server and @locel/client. The configuration codes belong to packages that do not exist yet.

Shape

import { errors } from '@locel/core'

interface LocelError extends Error {
  code: string
  status?: number      // HTTP status, when raised across the wire
  help?: string        // what to do about it
  collection?: string
  key?: string
  field?: string
  transaction?: string
}

help is populated for every code in this page. An error a developer has to search for is an error that was half-reported.

How codes are named

The grammar is the AdonisJS framework's, so a locel code reads like the ones already in your stack trace. New codes follow one of these, in this order of preference:

FormUseExamples
E_MISSING_<thing>Something required is absentE_MISSING_PARTITION_COLUMN, E_MISSING_SUBSET_INDEX
E_UNKNOWN_<thing>Named, but not recognisedE_UNKNOWN_COLLECTION, E_UNKNOWN_IDENTITY
E_INVALID_<thing> / E_UNSUPPORTED_<thing>Present but unusableE_INVALID_VALUE, E_UNSUPPORTED_DIALECT
E_<subject>_NOT_<state>A subject is not in a required stateE_ROW_NOT_FOUND, E_FIELD_NOT_WRITABLE
E_<subject>_<state>A subject is in a bad stateE_ROW_DELETED, E_STORE_TORN
E_<subject>_ALREADY_<state>Done twiceE_COLLECTION_ALREADY_DEFINED
E_CANNOT_<verb>_<noun>An action is structurally impossibleE_CANNOT_SEAL_PARTITION_COLUMN
E_<subject>_MISMATCHTwo things that must agree do notE_SCHEMA_MISMATCH, E_FINGERPRINT_MISMATCH

A subject goes in front when the bare phrase would be ambiguous outside locel — E_ROW_OUTSIDE_PARTITION rather than E_OUTSIDE_PARTITION, the same reason the framework says E_OAUTH_STATE_MISMATCH rather than E_STATE_MISMATCH.

Declaration errors

Raised by defineCollection and defineSchema, at module evaluation — so they fail the process at startup rather than on the first write.

CodeConditionWhat to do
E_RESERVED_FIELD_NAMEA field name starts with $Rename it. $ is reserved for magic columns.
E_UNKNOWN_PRIMARY_KEY_FIELDprimaryKey names a field that does not exist
E_INVALID_PRIMARY_KEY_TYPEprimaryKey names a field that is not field.string() or field.number()A row is addressed by its key, and only those two address anything.
E_UNKNOWN_WRITABLE_FIELDwritable names a field that does not existUsually a rename that missed the allowlist.
E_SERVER_OWNED_FIELD_NOT_WRITABLEA serverOwned() field appears in writableRemove it from writable. The two are contradictory.
E_CANNOT_RESOLVE_SEALED_FIELDA resolve entry for an encrypted() fieldThe server cannot read the plaintext. Sealed fields are always server-wins.
E_UNKNOWN_INDEX_FIELDAn index names a field that does not exist
E_CANNOT_INDEX_SEALED_FIELDAn index names an encrypted() fieldThe server holds an opaque envelope, so it cannot index, order or filter on it.
E_COLLECTION_ALREADY_DEFINEDTwo collections in one schema share a nameThe wire name must be unique.

Boot errors

Raised by createAuthority, before the server accepts a request.

CodeConditionWhat to do
E_MISSING_PARTITION_COLUMNA collection does not carry every partition columnAdd the column, or leave the collection out of the schema.
E_CANNOT_SEAL_PARTITION_COLUMNA partition column is encrypted()The server must be able to read it to enforce the boundary.
E_STORE_CANNOT_QUERYA store cannot serve an operation the configuration needs — most often query for subset modeUse a store that implements it. Never returns an empty result instead: an empty subset and a broken one look identical to a client.
E_UNSUPPORTED_DIALECTA knex or kysely store on a dialect the adapter has no profile forRaised at boot. The profile decides how sequence number assignment is serialised, and guessing would produce duplicate sequence numbers under load rather than an error. Open an issue naming the dialect.

Write refusals

Returned from push and rejected on tx.confirmed. Each rejects the whole transaction; the client rolls it back and removes it from the outbox.

CodeStatusConditionWhat to do
E_UNKNOWN_COLLECTION400The collection is not in the server's schemaAlmost always a client running ahead of a deploy. Deploy the server first.
E_ROW_OUTSIDE_PARTITION403The row's partition columns do not match the session's scopeA bug or an attack. Do not retry.
E_FIELD_NOT_WRITABLE403The patch touches a field absent from writable, or one marked serverOwned()Fix the caller. The field is not trimmed and applied — a silent trim hides the bug until someone reads the wrong value.
E_POLICY_REFUSED403A write policy returned falseShow the user why, if you can.
E_ROW_NOT_FOUND404Update or delete against a row that does not exist
E_ROW_EXISTS409Insert against a key that already existsTwo devices minted the same primary key, or the code inserted twice. Never silently upserted — that would put two users on one row.
E_ROW_DELETED409Update whose base version predates a concurrent delete, with onDeleteConflict: 'reject'Tell the user. This is the case where quiet resolution loses someone's paragraph.
E_INVALID_VALUE422A value fails its declared type — a non-finite number, null in a non-nullable field
E_SCHEMA_MISMATCH409The transaction was authored against a schema the server no longer hasThe client rebuilds and replays; entries that no longer apply are reported through $conflict.

None of these is a routine outcome. The routine one — somebody else edited the same field first — is a merge, and it does not appear here because it is not a refusal. See Conflicts.

Do not retry any of them automatically. Every one will fail again the same way, and a retry loop against E_ROW_OUTSIDE_PARTITION is a request storm.

Attach errors

Raised by replica.attach(), before anything is read or written.

CodeConditionWhat to do
E_FINGERPRINT_MISMATCHThe store found for this key holds a different schema hash, protocol version or storage formatreplica.rebuild(). Set rebuildOnMismatch in development; leave it off in production, where a mismatch means something you did not expect.
E_STORE_TORNSome $version or outbox base version exceeds the checkpointreplica.rebuild(). Should be unreachable — one transaction per write means there is nothing to be halfway through. If you see it, something outside locel wrote to the store, or the storage layer lied about a commit.
E_PROTOCOL_MISMATCHThe server speaks a different protocol versionDeploy order. A client on the old protocol against a new server is supported for the length of a rollout; the reverse is not.
E_BROKER_UNAVAILABLEThe requested broker is not available in this environmentOne process must own the replica. broker: 'none' is correct only where this process is the only one.
E_UNKNOWN_IDENTITYA first-ever start with no network, nothing cached from a previous hello, and no scope option — so neither the scope nor the principal is known, and there is no way to tell which store to openRender the signed-out or first-run state. Pass scope at construction if the application already knows it. Only reachable once per install — after one successful hello the scope is cached.

Subset errors

CodeConditionWhat to do
E_MISSING_SUBSET_INDEXNo declared index can serve the subset's filters and orderingAdd the index to the collection. Refused at open rather than run as an unbounded scan on every commit.
E_SUBSET_MODE_REQUIREDopenSubset on a replica in partition modeMode is fixed at construction and is part of the fingerprint.

Configuration errors

CodeConditionWhat to do
E_COLLECTION_ALREADY_PERSISTEDlocelCollectionOptions wrapped in persistedCollectionOptionsRemove the wrapper. The replica is the durable store; a second one is a second opinion about what your user wrote.
E_UNKNOWN_STORElocel.use(name) for a store not in the configNormally caught at compile time by the declare module block.
E_COLLECTION_NOT_IN_SCHEMAA collection passed to locelCollectionOptions is not in the replica's schema

Store failures

Not thrown — reported through replica.onStoreFailure, because they are conditions rather than events, and because throwing from a write that is already on screen would be too late.

reasonConditionRecovery
quotaOrigin storage is fullFree space, then replica.resume().
unavailableStorage cannot be opened at all — a disabled API, a private-mode restrictionDegrade explicitly. Do not fall back to memory silently.
corruptThe store cannot be readreplica.rebuild().

While a storage failure is outstanding the replica stops accepting writes. It does not drop outbox entries, prune confirmed rows, or continue in memory. See Offline and durability.

Handling them

A Rejection is a value, not a thrown error, so it is matched on code. It arrives twice, once as an event and once as durable state, and the two are for different jobs:

// Once, when the server answers. For telemetry and anything time-sensitive.
replica.onRejection((rejection) => {
  telemetry.count('locel.rejected', { code: rejection.code })
})

// Durable, until acknowledged. For telling the user.
for (const rejection of replica.rejections) {
  render(rejection.code === 'E_POLICY_REFUSED'
    ? 'You do not have permission to do that.'
    : 'Some changes could not be saved.')
}

Two rules, everywhere:

Never retry a refusal automatically. They are bugs and races, not transient failures.

Never discard one silently. A user who ticked three todos on a train is owed the information that one did not stick, even if the other two did — which is why the account is a list you clear with replica.acknowledge(rejection), and not a toast that fires once while the tab is closing.

The grammar comes from ADR 0014.

On this page