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,errorandmessage. Checksuccess. - 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 atry/catchsees every failure. Pass it togetkey(),createkey(), a sign-in orPrefsDB.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
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: