The Python SDK raises an exception from one class hierarchy for every failed operation, from a refused query to a server that does not answer. This page shows which class each kind of failure raises, how to catch the ones your code can handle, and which ones are worth retrying. The error kinds, wire codes and structured shape behind server errors are documented once for every SDK and protocol in Errors.
API references
Class or method | Description |
|---|---|
SurrealError | Base class for every error the SDK raises |
ServerError | A failure the server reported, with a kind, details and a cause chain |
TransportError | A request that got no answer from the server |
HttpStatusError | An HTTP error status, such as 401 for a refused token |
UnsupportedFeatureError | A feature the connection type does not have |
UnavailableFeatureError | A feature the server version does not have |
ErrorKind | Constants for the kind of a server error |
db.query_raw() | Returns the status of each statement in place of raising |
The error hierarchy
Every exception the SDK raises for a failed operation is a subclass of SurrealError, so except SurrealError catches all of them. Below it, two branches separate a failure that the server reported from a request that got no answer:
SurrealError
├── ServerError
│ ├── ValidationError, ConfigurationError, ThrownError
│ ├── QueryError, SerializationError, NotAllowedError
│ └── NotFoundError, AlreadyExistsError, InternalError
├── TransportError
│ ├── ConnectionUnavailableError
│ ├── TransportTimeoutError
│ └── HttpStatusError
├── UnsupportedFeatureError
│ └── UnavailableFeatureError
├── UnsupportedVersionError, UnsupportedEngineError
├── InvalidUrlError, ImportFailedError, UnexpectedResponseError
├── InvalidRecordIdError, InvalidDurationError
└── InvalidGeometryError, InvalidTableErrorThe split tells your code what to do next. A ServerError means that the server ran the request and refused it, so the same request fails again. A TransportError means that no answer arrived: the server could not be reached, did not reply in time, or answered with an HTTP error status. The same request can succeed when you send it again. In SDK 2.x, most of these failures raised exceptions from libraries such as requests and aiohttp, so see Migrating to 3.x if your code catches those. The Errors reference lists every class with its properties.
A mistake in the arguments of a call is not a SurrealError. It raises Python's own TypeError or ValueError, as described in Mistakes in a call.
Where an error surfaces
A query passes through two layers, and a failure can come from either one. The first is the request itself: a connection that is unavailable, a rejected sign-in, a query that does not parse. The second is the individual statements inside the query, which can fail while the request as a whole succeeds.
.execute() turns both into an exception. It raises the error of the first statement that failed, so the results of the statements that succeeded are not returned. The other statements still ran, and their changes stay: after CREATE post:one; THROW 'stop'; CREATE post:two, both records exist although .execute() raised. Use a transaction or batch() when statements must succeed or fail together.
Where the outcome of each statement matters, query_raw() returns every statement in place of raising. Each entry carries a status of OK or ERR, and a failed one also carries its kind.
from surrealdb import Surreal, ThrownError
with Surreal("ws://localhost:8000") as db:
db.signin({"username": "root", "password": "secret"})
db.use("surrealdb", "docs")
# .execute() raises the error of the first statement that failed.
try:
db.query("RETURN 1; THROW 'second'").execute()
except ThrownError as error:
print(f"{error.kind}: {error}")
# query_raw() reports every statement instead of raising.
response = db.query_raw("RETURN 1; THROW 'second'")
for index, statement in enumerate(response["result"]):
print(index, statement["status"], statement["result"])Thrown: An error occurred: second
0 OK 1
1 ERR An error occurred: secondquery_raw() does not raise when the server refuses the whole request either. A query that does not parse comes back with an error key, holding the kind and message, in place of result.
Catching server errors
The server gives every error a kind, and the SDK raises a class of its own for each of the kinds below. Errors describes the meaning of each kind. Match on the class or the kind rather than on the message, which can change between releases.
| Kind | Exception class |
|---|---|
Validation | ValidationError |
Configuration | ConfigurationError |
Query | QueryError |
Serialization | SerializationError |
NotAllowed | NotAllowedError |
NotFound | NotFoundError |
AlreadyExists | AlreadyExistsError |
Thrown | ThrownError |
Internal | InternalError |
A kind without a class of its own, such as Connection or a kind added by a newer server, arrives as the base ServerError with its kind set, so except ServerError catches every server error on every server version.
The examples in the rest of this page use an open connection, db, signed in and using the surrealdb namespace and the docs database. Each class has properties that read the reason out of the error's details. Creating a record that already exists raises AlreadyExistsError, with the id of the record:
from surrealdb import AlreadyExistsError, RecordID
db.upsert(RecordID("person", "tobie"), {"name": "Tobie"})
try:
db.create(RecordID("person", "tobie"), {"name": "Tobie"})
except AlreadyExistsError as error:
print(error.record_id)person:tobieFrom SurrealDB 3.0, selecting from a table that does not exist raises NotFoundError, also when you select one record of it. A record that does not exist in a table that does is not an error: select() returns None for it.
from surrealdb import NotFoundError, Table
try:
db.select(Table("post_archive")).execute()
except NotFoundError as error:
print(error.table_name)post_archiveA sign-in with a wrong password raises NotAllowedError. Its properties give the reason, such as is_invalid_auth for wrong credentials and is_token_expired for an expired token:
from surrealdb import NotAllowedError, Surreal
with Surreal("ws://localhost:8000") as db:
try:
db.signin({"username": "root", "password": "wrong"})
except NotAllowedError as error:
print(error.is_invalid_auth, error)True There was a problem with authenticationA record user whose SIGNIN clause matches no record gets NotFoundError: No record was returned instead. Catch ServerError around a record sign-in when the reason does not matter to your code. Authentication covers record users.
Matching on the kind
Every ServerError has a kind string. Compare it with the ErrorKind constants to test for a kind without importing its class. has_kind() is True when the error, or any error in its chain of causes, has the kind, and find_cause() returns the first such error, or None. The cause chain is in server_cause.
from surrealdb import ErrorKind, ServerError, Table
try:
db.select(Table("post_archive")).execute()
except ServerError as error:
if error.has_kind(ErrorKind.NOT_FOUND):
print(error.find_cause(ErrorKind.NOT_FOUND).details){'details': {'name': 'post_archive'}, 'kind': 'Table'}details holds the structured reason that the properties of each class read. It is None when the server sends no reason: for example, a parse error in a query arrives as a ValidationError without details. The detail kind constants name the reasons.
Retrying transport errors
TransportError has three subclasses. Each keeps the exception of the underlying library, such as a ConnectionRefusedError, as __cause__.
| Class | Raised when |
|---|---|
ConnectionUnavailableError | The server cannot be reached, the socket closes while a request waits for its reply, or a live subscription is read after its connection was lost |
TransportTimeoutError | No reply arrived within the connection's timeout, or within a db.timeout() block |
HttpStatusError | An HTTP connection got an error status, such as 401 |
A server that cannot be reached raises ConnectionUnavailableError on the first request, or on connect():
from surrealdb import ConnectionUnavailableError, Surreal
db = Surreal("ws://localhost:8001")
try:
db.connect()
except ConnectionUnavailableError as error:
print(error)could not connect to ws://localhost:8001/rpc: [Errno 61] Connection refusedA refused token shows why the two branches are separate. Over a WebSocket, authenticate() with a token the server refuses raises NotAllowedError, because the server answers in its structured error format. Over HTTP, the server answers with status 401 and no structured error, so the same call raises HttpStatusError. In the example below, token holds an access token that the server refuses:
from surrealdb import HttpStatusError, Surreal
with Surreal("http://localhost:8000") as db:
try:
db.authenticate(token)
except HttpStatusError as error:
print(error.status, error.body)401 There was a problem with authenticationWhen a WebSocket connection drops, the SDK opens it again on its own, as described in Automatic reconnection. A request that was in flight when the socket dropped is not sent again, because it may already have run, and raises ConnectionUnavailableError.
A TransportError does not tell you whether the server ran the request. A write that timed out may still have been applied, so retry only statements that give the same result when they run twice, such as an upsert() of a whole record:
import time
from surrealdb import RecordID, TransportError
def upsert_with_retry(db, record, data, attempts=3):
for attempt in range(attempts):
try:
return db.upsert(record, data)
except TransportError:
if attempt == attempts - 1:
raise
time.sleep(2**attempt)
upsert_with_retry(db, RecordID("person", "tobie"), {"name": "Tobie"})A ServerError from upsert() passes through at once, because sending the same request again gives the same answer.
Two kinds of timeout raise different classes. TransportTimeoutError means that the SDK stopped waiting for a reply, which can happen while the server still runs the request. A TIMEOUT clause in a statement is enforced by the server, which reports the exceeded limit as a QueryError:
from surrealdb import QueryError
try:
db.query("SELECT * FROM sleep(500ms) TIMEOUT 50ms").execute()
except QueryError as error:
print(error.is_timed_out, error.timeout)True 50msFeatures the connection or server does not have
An UnsupportedFeatureError means that the type of connection does not have the feature: for example, sessions, transactions and live queries need a WebSocket, so an HTTP connection raises it for them. Its subclass UnavailableFeatureError means that the server version does not have the feature, such as sessions on SurrealDB 2.x. The SDK raises both before it sends anything.
UnavailableFeatureError names the feature, the version of the server and the first version with the feature. Put its except clause before the one for UnsupportedFeatureError, which would otherwise catch it first:
from surrealdb import Surreal, UnavailableFeatureError, UnsupportedFeatureError
with Surreal("ws://localhost:8000") as db:
db.signin({"username": "root", "password": "secret"})
try:
session = db.new_session()
except UnavailableFeatureError as error:
print(error.feature, error.version, error.since)
except UnsupportedFeatureError as error:
print(error)sessions 2.7.0 3.0.0To check first, call db.is_feature_supported() with a constant such as Features.SESSIONS.
The SDK supports SurrealDB 2.x, 3.x and 4.x. Connecting to a server on any other version raises UnsupportedVersionError before the first request. Pass version_check=False to the connection to connect anyway.
Errors in the connection URL
Surreal() and AsyncSurreal() check the URL before they connect. A scheme the SDK does not know raises UnsupportedEngineError, and so does an embedded URL such as mem:// when the surrealdb[embedded] package is not installed. A URL with a known scheme that cannot be parsed, such as one with a port out of range, raises InvalidUrlError:
from surrealdb import InvalidUrlError, Surreal, UnsupportedEngineError
for url in ("ftp://localhost:8000", "ws://localhost:99999"):
try:
Surreal(url)
except UnsupportedEngineError as error:
print("Unsupported:", error.url)
except InvalidUrlError as error:
print("Invalid:", error)Unsupported: ftp://localhost:8000
Invalid: Could not parse URL 'ws://localhost:99999': Port out of range 0-65535Errors from an import
A statement that fails in import_() does not stop the rest of the import. When the import ends, import_() raises ImportFailedError, with the error of each failed statement in failures. The statements that did not fail were applied. Import and export describes imports. The import below creates person:tobie, which already exists:
from surrealdb import ImportFailedError
try:
db.import_("OPTION IMPORT; CREATE person:tobie SET name = 'Tobie';")
except ImportFailedError as error:
print(len(error.failures), type(error.failures[0]).__name__)1 AlreadyExistsErrorMistakes in a call
A call that the SDK can tell is wrong before it sends anything raises Python's own TypeError or ValueError, not a SurrealError. These are mistakes to fix in the code rather than failures to handle, so except SurrealError lets them through. Examples are a value the SDK cannot encode, arguments of the wrong type, and an empty batch():
from datetime import timedelta
try:
db.query("RETURN $wait", {"wait": timedelta(seconds=5)}).execute()
except TypeError as error:
print(error)cannot encode timedelta for SurrealDB; supported types are the JSON scalars, list, dict, set, bytes, date, time, and the surrealdb data types (RecordID, Table, Datetime, Duration, Range, Geometry, Decimal, UUID, the sized numbers, PlainDate, PlainTime, Timestamp, Interval, BigDecimal, Regex)Send a Duration in place of a timedelta.
Permission denials return empty results
A record user whose table PERMISSIONS deny a statement does not get an error. The server leaves out what the user may not see or change: the statement succeeds with an empty result, query_raw() reports status as OK, and nothing is raised.
# `user` is signed in as a record user who may not update this post
print(user.query("UPDATE post:draft SET published = true").execute())[[]]The server does this so that an error cannot reveal what a permission rule hides. An error only for a record that exists would tell the user that the record exists. A denied UPDATE of an existing record and an UPDATE of a record that does not exist therefore give the same result.
An empty result from a write does not tell you whether anything was written. A user who may create a record but not select it gets an empty result for a record that was created. To check the outcome, read the record on a connection that can see it, such as one signed in as root, which table permissions do not limit.
SurrealDB 2.x servers
The classes below ServerError come from the kind that the server reports, and SurrealDB 2.x sends no kind. On a 2.x server, every server error is therefore an InternalError:
| Failure | SurrealDB 3.x and 4.0 | SurrealDB 2.x |
|---|---|---|
THROW 'nope' | ThrownError | InternalError |
| A query that does not parse | ValidationError | InternalError |
| Creating a record that exists | AlreadyExistsError | InternalError |
signin() with a wrong password | NotAllowedError | InternalError |
select() of a table that does not exist | NotFoundError | Returns [] |
except ServerError and except SurrealError work on every version. To support SurrealDB 2.x, catch those rather than a subclass such as ThrownError, which a 2.x server never causes.
Learn more
Errors reference for every error class and its properties
ErrorKind constants for matching on the kind
Errors for the error kinds, details and wire codes of every protocol
Connecting to SurrealDB for timeouts and reconnection
Transactions for statements that succeed or fail together