Skip to content

Overview

Errors

Every error SurrealDB returns has the same shape, whichever protocol carries it. The SDKs map that shape onto their own idioms, so the vocabulary on this page is the one behind every SDK's error type.

The preferred way to handle an error is to branch on kind first and code second, both of which are a stable contract. The message is written for a person reading it and is free to change between releases, so treat it as text to display rather than something to match on.

A request has two layers that can fail independently, and they report failure differently.

The call can fail as a whole: incorrect syntax, an unknown method, a rejected sign-in. In this case the response will carry an error object in place of a result.

Call-level error
{
    "error": {
        "cause": null,
        "code": -32603,
        "details": { "kind": "InvalidParams" },
        "kind": "Validation",
        "message": "Expected (what, data)"
    }
}

Once the call succeeds, individual statements inside will be either successes or failures. Each statement reports its own status, and a failing one carries its kind with the message in result. Statements that ran before it keep their results.

Take these three statements for example which can be sent as one call. The field definition and the first CREATE statement are accepted, and only the third breaks the assertion:

DEFINE FIELD name ON user TYPE string ASSERT $value.len() <= 20;
CREATE user:short SET name = "Billy";
CREATE user:long SET name = "Mr. Muchtoolongname the Fourth";

The call itself succeeded, so there is no error object. The failure is reported against the one statement that caused it, and user:short is still created:

Statement-level error
{
    "result": [
        { "result": null, "status": "OK", "time": "7.899708ms", "type": null },
        {
            "result": [{ "id": "user:short", "name": "Billy" }],
            "status": "OK",
            "time": "10.369375ms",
            "type": null
        },
        {
            "kind": "Internal",
            "result": "Found 'Mr. Muchtoolongname the Fourth' for field `name`, with record `user:long`, but field must conform to: $value.len() <= 20",
            "status": "ERR",
            "time": "1.351458ms",
            "type": null
        }
    ]
}

A failed statement does not stop the ones after it, and does not roll back the ones before it. Statements are independent unless a transaction makes them otherwise.

Wrapping the same two CREATE statements in BEGIN and COMMIT ties their fates together:

DEFINE FIELD name ON user TYPE string ASSERT $value.len() <= 20;

BEGIN;
CREATE user:short SET name = "Billy";
CREATE user:long SET name = "Mr. Muchtoolongname the Fourth";
COMMIT;

The call still succeeds, so there is still no error object. What changes is that the statement which would have worked on its own now reports NotExecuted, and the COMMIT refuses:

Statement errors inside a transaction
{
    "result": [
        { "result": null, "status": "OK", "time": "8.304667ms", "type": null },
        { "result": null, "status": "OK", "time": "0ns", "type": null },
        {
            "details": { "kind": "NotExecuted" },
            "kind": "Query",
            "result": "The query was not executed due to a failed transaction",
            "status": "ERR",
            "time": "10.880333ms",
            "type": null
        },
        {
            "kind": "Internal",
            "result": "Found 'Mr. Muchtoolongname the Fourth' for field `name`, with record `user:long`, but field must conform to: $value.len() <= 20",
            "status": "ERR",
            "time": "923.041µs",
            "type": null
        },
        {
            "details": { "kind": "NotExecuted" },
            "kind": "Query",
            "result": "Cannot COMMIT: the transaction was aborted due to a prior error",
            "status": "ERR",
            "time": "0ns",
            "type": null
        }
    ]
}

Neither record exists afterwards, user:short included. The DEFINE FIELD is untouched, because it ran before BEGIN: only the statements inside the transaction are rolled back.

This split is why each SDK offers two ways to read a query result: one that surfaces the first failure, and one that reports every statement. The names differ per language, and each SDK's error page covers its own.

The HTTP endpoints /sql and /key use the same two layers. When the request itself fails, the response has an HTTP error status: 400 for a parse error, 401 for credentials that do not authenticate, and 413 for a body over the size limit. Once the request is accepted, the response is 200 OK even when a statement fails. This includes a THROW, a record that already exists, a namespace or table that does not exist, and a statement that runs past --query-timeout. Each failure is reported in that statement's entry in the response array, with status set to "ERR":

curl -X POST -u "root:secret" -H "Accept: application/json" -d 'THROW "Order not found"' http://localhost:8000/sql
Response
[
    {
        "kind": "Thrown",
        "result": "An error occurred: Order not found",
        "status": "ERR",
        "time": "101.75µs",
        "type": null
    }
]

A GET /key/:table/:id for a record that does not exist is not an error. It returns 200 with an empty result array. Code that calls these endpoints must therefore check status on each entry as well as the HTTP status.

FieldDescription
kindThe error category. The primary thing to branch on.
codeNumeric wire code, kept for backwards compatibility.
messageA human-readable description liable to change. Be sure not to match on it unless you are able to update the match when upgrading versions.
detailsStructured detail for kinds that carry one, itself carrying a nested kind.
causeThe underlying error, where one was attached. Nested errors use this same shape.
KindMeaning
ValidationParse error, invalid request, or invalid parameters
ConfigurationA feature or configuration is not supported
QueryA query timed out, was cancelled, or was not executed
SerializationA value could not be serialised or deserialised
NotAllowedA permission or authorisation check failed
NotFoundA resource does not exist
AlreadyExistsA resource already exists
ConnectionA client-side connection failure
ThrownA THROW statement ran in SurrealQL
InternalAn internal or unexpected failure
ContextA wrapper carrying context around another error

Internal doubles as the catch-all for a kind the reader does not recognise, so code that handles the kinds it cares about and treats the rest as internal keeps working against a newer server.

details names the specific reason within the kind. It has its own kind, and its own details where the reason carries values. Details nest, so a failed sign-in over RPC is a NotAllowed error whose reason is Auth, whose reason in turn is InvalidAuth:

A failed sign-in
{
    "kind": "NotAllowed",
    "code": -32002,
    "message": "There was a problem with authentication",
    "details": {
        "kind": "Auth",
        "details": { "kind": "InvalidAuth" }
    },
    "cause": null
}

A reason with values carries them in the inner details. A duplicate record, for example, gives { "kind": "AlreadyExists", "details": { "kind": "Record", "details": { "id": "user:a" } } }.

KindReasonValues
ValidationParse, InvalidRequest, InvalidParams, NamespaceEmpty, DatabaseEmpty
ValidationInvalidParametername
ValidationInvalidContent, InvalidMergevalue
ConfigurationLiveQueryNotSupported, BadLiveQueryConfig, BadGraphqlConfig, DialectNotSupported (from 3.3.0)
QueryNotExecuted, Cancelled, TransactionConflict
QueryTimedOutduration
SerializationSerialization, Deserialization
NotAllowedScripting
NotAllowedMethod, Function, Targetname
NotAllowedAuthA further reason, listed below
NotFoundMethod, Table, Namespace, Databasename
NotFoundRecordid
NotFoundSessionid, which can be empty
NotFoundTransaction
AlreadyExistsTable, Namespace, Databasename
AlreadyExistsRecord, Sessionid
ConnectionUninitialised, AlreadyConnected, ConnectionFailed

Connection errors are raised by an SDK on the client, not sent by the server. Thrown, Internal and Context carry no reason.

The reasons inside Auth are:

ReasonValues
TokenExpired, SessionExpired, InvalidAuth, UnexpectedAuth, MissingUserOrPass, NoSigninTarget, InvalidPass, TokenMakingFailed, InvalidSignup
InvalidRolename
NotAllowedactor, action, resource

A few reasons tell a client what to do next. TransactionConflict means that the operation can be retried. TokenExpired and SessionExpired mean that the client needs to sign in again.

Not every error carries a reason. A parse error sent over RPC, for example, arrives as a Validation error with no details, so check that details is present before reading it. New reasons are added as the server grows, so treat a reason you do not recognise as an error of its kind, in the same way as an unknown kind is treated as Internal.

CodeName
-32700Parse error
-32600Invalid request
-32601Method not found
-32602Method not allowed
-32603Invalid parameters
-32604Live query not supported
-32605Bad live query configuration
-32606Bad GraphQL configuration
-32000Internal error
-32001Client-side error
-32002Invalid authentication
-32003Query not executed
-32004Query timed out
-32005Query cancelled
-32006Thrown
-32007Serialization error
-32008Deserialization error
-32009Query transaction conflict
-32607Dialect not supported (from 3.3.0)

Two of these differ from the JSON-RPC conventions they resemble: -32602 is method-not-allowed rather than invalid parameters, and invalid parameters is -32603. A code also does not always follow from the kind, since an error can be a Validation while carrying the generic -32000. Reading kind avoids both surprises.

Each SDK exposes these kinds through its own error type, and documents the two-layer behaviour in its own idiom.

Was this page helpful?