Skip to content

Error Codes

When something goes wrong, error is a code that stays the same (check this one in your code) and message explains it in words, e.g.

{
    "success": false,
    "status": "error",
    "ts": "2026-05-18 19:22:48",
    "error": "MISSING_API_KEY",
    "message": "Missing API key"
}

How the JavaScript client reports errors

The rule, for every method:

  • The server refused the request (a bad key, too many requests, a value too large): the Promise resolves with success: false, error and message. Check success.
  • The request couldn't be made, or got no answer (offline, a timeout): the Promise rejects. The exceptions are writes and reads of one key, which offline mode handles instead: see the last paragraph.
  • With throwOnError: true, both reject, so a try/catch sees every failure. Pass it to getkey(), createkey(), a sign-in or PrefsDB.client(); every request made with that object follows it.
const db = await PrefsDB.getkey("myapp", seed, { email, throwOnError: true });
try {
    await db.map("todos").put(id, todo);
} catch (err) {
    if (err.code === "QUOTA_EXCEEDED") wait(err.retryAfter);
    else rollback(id);
}

A few methods reject on any failure, with or without throwOnError, because they resolve to something other than an answer: getkey(), createkey(), the sign-ins and rotate() (a PrefsDB object), entries() (an array), getObject() (a file), getObjectUrl() (a link) and waitForVerification().

Writes saved for later while offline (queued: true) and optimistic writes (optimistic: true) resolve either way: they haven't failed. What happens to them afterwards is reported by db.onSyncEvent(); see Offline and Optimistic Writes.

Whatever rejects is a PrefsDBError, a JavaScript Error with the answer's fields plus:

Field What it holds
code The same code as error.
httpStatus The HTTP status of the server's answer, or null when there was none.
retryAfter For a 429, how many seconds to wait before trying again; otherwise null.
ref For a 500, a reference to quote to PrefsDB support; otherwise null.

err instanceof PrefsDBError tells it apart from a bug in your own code. An answer that resolves with success: false has httpStatus too. The sign-in buttons pass the same kind of error to onError.

API key errors

The server answers with these when the key, or what you asked of it, can't be served:

error Status What it means
MISSING_API_KEY 400 No key was sent with the request.
UNKNOWN_API_KEY 400 The key doesn't exist, or it was deleted in the dashboard. A deleted key can be restored from the dashboard for 30 days.
RESERVED_KEY_NAME 400 A key named with one of the reserved words (such as name, list or type). Choose another name. The JavaScript client throws this before sending anything.
SESSION_REQUIRED, SESSION_EXPIRED 401 A key from getkey() used without its short session, or after it ran out. The JavaScript client gets a new one by itself; elsewhere, call getkey() again.
SIGNIN_EXPIRED 401 A key from signing in with Google, Microsoft, Apple or GitHub can renew its session by itself for 7 days after the sign-in. After that, ask the user to sign in again.
UNVERIFIED 403 A new key whose email hasn't been confirmed yet. Click the link in the email.
SPONSORSHIP_LAPSED 403 A key created without an email on a site that no longer sponsors its users' keys.
KEY_SUSPENDED 403 A dashboard key that is suspended. New Live keys start this way. Activate it in the dashboard.
KEY_EXPIRED 403 A dashboard key whose expiry date has passed. Create a new key in the dashboard. See Restricted API Keys.
KEY_READ_ONLY 403 A read-only dashboard key was used to change something.
INVALID_DEVICE_CODE 400 Connect an AI Agent: the pairing request ended (approved and collected, denied, or expired). Start again.
SCOPE_NOT_ALLOWED 403 A dashboard key limited to a scope, used without naming exactly that scope. The response's scope says what to send.
ACCOUNT_SUSPENDED 403 The account that issued this dashboard key is suspended, so none of its keys work. Contact PrefsDB support.
QUOTA_EXCEEDED 429 The key used up its requests or data transfer for the minute, day or month. For a key from a dashboard account, the account's keys share one allowance, so this can also mean they used it up together. The response says which.
RATE_LIMITED 429 Too many attempts in a short time, for example creating keys.
KEY_UNAVAILABLE 500 getkey() found your key but couldn't hand it back. The key itself still works. Contact PrefsDB support.
IDTOKEN_INVALID 401 Sign in with Google, Microsoft, Apple or GitHub: the sign-in couldn't be checked, or was too old. Sign in again (for Google, Microsoft and Apple, with a new idTokenNonce()).
IDTOKEN_REPLAYED 401 That sign-in was already used. Sign in again.
IDTOKEN_EMAIL_UNVERIFIED 403 The Google account's email address isn't confirmed.
IDTOKEN_KEYS_UNAVAILABLE 503 Sign-in with Google, Microsoft or Apple is briefly unavailable. Try again shortly.
FILTER_UNAVAILABLE 400 A .match() pattern with %, for a key whose values are stored encrypted. Only exact matches work there (see Filtering data).
CONFLICT 409 A change with ifVersion found the value changed (or deleted, or for ifVersion: 0 already there) since that version, so nothing was changed. The answer's current holds the value and version now. See Several Devices.
BAD_VERSION 400 ifVersion isn't a version from a read.
CONDITION_NOT_SUPPORTED 400 ifVersion on a list: list entries have no version.
CHANGES_EXPIRED 410 changes({ since }) from longer ago than the 30 days of deletions PrefsDB keeps. Read the whole map again and continue from its changeSeq. watch() does this by itself.
BAD_SINCE, SINCE_NOT_SUPPORTED 400 changes() was given something that isn't a changeSeq, or was asked about a list (it covers keys and maps).
DEVICE_GRANT_INVALID 401 PrefsDB.resume(): this device was signed out or removed from the list of devices, or its 30 days (or the days asked for) ran out. resume() resolves to null then; sign in again. See Staying Signed In.

Every 429 answer says how long to wait: the Retry-After header and the retryAfter field give the number of seconds. Try again after that.

{
    "success": false,
    "status": "error",
    "ts": "2026-10-02 09:15:00",
    "error": "KEY_SUSPENDED",
    "message": "This API key is suspended. Its owner can activate it in the dashboard."
}

General error structure

{ 
    success : Boolean,
    ts      : Number, 
    error   : "String",
    message : "String"
}

Errors from the JavaScript client

These come from the client itself, not the server, when a request couldn't be made or got no usable answer. Like the server's, each has a code in error. ts is then a JavaScript timestamp in milliseconds.

error What it means
TIMEOUT No answer within the timeout option.
ABORTED The request was cancelled, e.g. the page was reloaded.
NETWORK_ERROR The request couldn't be sent: the device is offline, or the server couldn't be reached.
HTTP_ + status, e.g. HTTP_502 The answer wasn't from PrefsDB (a proxy's error page, say). httpStatus has the HTTP status.
OFFLINE Offline mode: no copy of this value (or this map) in the browser yet.
CONFLICT A write's settled Promise: the change was saved offline, but someone changed the value on the server first, so theirs was kept. See Offline and Optimistic Writes. (A change with ifVersion gets the server's CONFLICT above.)
SIGNED_OUT A write's settled Promise: db.logout() deleted the change before it was sent.
INSECURE_CONTEXT getkey(), createkey() or rotate() were called from a plain http: page. Serve it over HTTPS (http://localhost is fine while you develop).
BAD_SEED, BAD_TOKEN, BAD_OPTIONS A call was given no seed, no ID token, or options that don't go together (such as both namespace and appcred). Nothing was sent.
CLIENT_ERROR Something else went wrong in the client; message says what.

For example:

{
    success    : false,
    ts         : 1772942415973,
    error      : "TIMEOUT",
    code       : "TIMEOUT",
    httpStatus : null,
    message    : "The operation timed out."
}