# Copyright 2025-2026 Aerospike, Inc.
#
# Portions may be licensed to Aerospike, Inc. under one or more contributor
# license agreements WHICH ARE COMPATIBLE WITH THE APACHE LICENSE, VERSION 2.0.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Typed exceptions for the SDK client.
Subclasses of :class:`AerospikeError` mirror common server and client outcomes so
callers can handle failures selectively (for example ``except GenerationError``)
instead of comparing result codes everywhere.
At public boundaries, errors from the underlying async client are normalized with
:func:`_convert_pac_exception`. Callers should chain causes explicitly:
``raise _convert_pac_exception(exc) from exc``.
"""
from __future__ import annotations
from aerospike_async.exceptions import (
AerospikeError as PacAerospikeError,
ConnectionError as PacConnectionError,
InvalidNodeError as PacInvalidNodeError,
ServerError as PacServerError,
TimeoutError as PacTimeoutError,
UDFBadResponse as PacUDFBadResponse,
)
from aerospike_async.exceptions import ResultCode
# ---------------------------------------------------------------------------
# Base
# ---------------------------------------------------------------------------
[docs]
class AerospikeError(Exception):
"""Base class for SDK failures.
Raised directly when no more specific subclass applies, including
unmapped server result codes (see :func:`_result_code_to_exception`).
Prefer catching concrete subclasses when you need targeted handling, and
fall back to this type for all other Aerospike-related errors.
Attributes:
result_code: Server :class:`~aerospike_async.exceptions.ResultCode` when
the failure came from a result code; ``None`` for purely client-side
issues (for example connection setup).
in_doubt: ``True`` when a write may have completed on the server despite
the error; safe retry usually requires a read-verify strategy.
Example::
try:
stream = await session.query(key).bins(["x"]).execute()
await stream.first_or_raise()
except AerospikeError as err:
code = err.result_code
...
See Also:
:func:`_result_code_to_exception`: Maps result codes to this type or a
subclass.
"""
[docs]
def __init__(
self,
message: str = "",
*,
result_code: ResultCode | None = None,
in_doubt: bool = False,
) -> None:
super().__init__(message)
self.result_code = result_code
self.in_doubt = in_doubt
# ---------------------------------------------------------------------------
# Timeout / connectivity
# ---------------------------------------------------------------------------
[docs]
class TimeoutError(AerospikeError):
"""Raised when an operation exceeds a client or server timeout.
Covers socket-level timeouts and server-reported timeout result codes.
This type shares a name with Python's built-in :exc:`TimeoutError`; always
import it from :mod:`aerospike_sdk` or this module when handling SDK
client timeouts.
Attributes:
result_code: Set when the server returned a timeout-related code;
otherwise often ``None`` for client-side timeouts.
See Also:
:class:`ConnectionError`: Cluster reachability rather than deadline
exceeded.
Example::
try:
await stream.first_or_raise()
except TimeoutError:
... # retry or fall back
"""
[docs]
class ConnectionError(AerospikeError):
"""Raised when the client cannot establish or keep a cluster connection.
Typical causes include refused sockets, TLS handshake failure, or loss of
connectivity mid-flight. Distinct from :class:`TimeoutError`, which signals
a deadline rather than an immediate transport failure.
Attributes:
result_code: Usually ``None`` because the failure occurs before a server
result code is available.
Example::
try:
async with Client(...) as client:
...
except ConnectionError:
... # cluster unreachable
"""
[docs]
class InvalidNodeError(AerospikeError):
"""Raised when the chosen node is unknown, wrong role, or not usable.
Use for diagnosing cluster topology or client routing issues rather than
application-level data errors.
Attributes:
result_code: Usually ``None``.
"""
[docs]
class InvalidNamespaceError(AerospikeError):
"""Raised when the namespace is missing or not defined on the cluster.
Often indicates a configuration mismatch between application and cluster.
Attributes:
result_code: Typically ``ResultCode.INVALID_NAMESPACE`` when mapped from
a server response.
Example::
try:
await session.query(bad_ds).execute()
except InvalidNamespaceError:
... # namespace not configured on cluster
"""
# ---------------------------------------------------------------------------
# Security
# ---------------------------------------------------------------------------
[docs]
class SecurityError(AerospikeError):
"""Base class for authentication, authorization, and security policy errors.
Several distinct server result codes collapse to this type when they do not
warrant a dedicated subclass. Catch :class:`AuthenticationError` or
:class:`AuthorizationError` first if you need finer granularity.
Attributes:
result_code: The security-related code returned by the server, when
applicable.
"""
[docs]
class AuthenticationError(SecurityError):
"""Raised when credentials are rejected or the session is not authenticated.
Examples include invalid user, expired password, or not authenticated
responses from the server.
See Also:
:class:`AuthorizationError`: Valid identity but disallowed operation.
"""
[docs]
class AuthorizationError(SecurityError):
"""Raised when the authenticated principal may not perform the operation.
Distinct from :class:`AuthenticationError`, which indicates identity or
credential problems rather than policy denial.
"""
# ---------------------------------------------------------------------------
# Data integrity
# ---------------------------------------------------------------------------
[docs]
class GenerationError(AerospikeError):
"""Raised when a write fails due to a record generation mismatch.
The record was modified since it was read, or the expected generation did
not match. Retrying blindly usually requires re-reading the record and
reapplying the logical update.
Attributes:
result_code: Typically ``ResultCode.GENERATION_ERROR``.
Example::
try:
await (
session.upsert(key)
.put({"x": 1})
.ensure_generation_is(3)
.execute()
)
except GenerationError:
... # record was modified by another writer
See Also:
:meth:`~aerospike_sdk.aio.session.Session.upsert`: Common write path
that can enforce generations on builders.
"""
[docs]
class QuotaError(AerospikeError):
"""Raised when a server-side quota or limit is exceeded.
Handling is usually operational (throttle, increase limits, or partition
workload) rather than a single-record retry.
"""
[docs]
class SerializationError(AerospikeError):
"""Raised when a value cannot be encoded for the wire or decoded from it.
Check bin types and application serializers when this appears on puts or
reads.
"""
# ---------------------------------------------------------------------------
# Operations
# ---------------------------------------------------------------------------
[docs]
class QueryTerminatedError(AerospikeError):
"""Raised when a query stops early (aborted, canceled, or server-terminated).
Partial rows may already have been delivered on streaming paths; this error
represents the overall query outcome, not a single-key failure inside a
batch.
Attributes:
result_code: May include ``ResultCode.QUERY_ABORTED`` or related codes.
"""
[docs]
class BackoffError(AerospikeError):
"""Raised when the server signals rate limiting or requires backoff.
Callers may retry after a delay or reduce request pressure.
"""
[docs]
class CommitError(AerospikeError):
"""Raised when a multi-record transaction commit does not complete successfully.
Additional fields expose verify or roll-forward details when the underlying
client provides them.
Attributes:
commit_error_type: Implementation-defined label for the failure phase,
if available.
verify_records: Verify-phase records or summaries, if available.
roll_records: Roll-forward or rollback-phase records, if available.
result_code: Server or client result associated with the commit, when set.
in_doubt: Inherited; ``True`` when commit outcome may be ambiguous on
the server.
"""
[docs]
def __init__(
self,
message: str = "",
*,
commit_error_type: object | None = None,
verify_records: list | None = None,
roll_records: list | None = None,
result_code: ResultCode | None = None,
in_doubt: bool = False,
) -> None:
super().__init__(message, result_code=result_code, in_doubt=in_doubt)
self.commit_error_type = commit_error_type
self.verify_records = verify_records
self.roll_records = roll_records
# ---------------------------------------------------------------------------
# Factory: ResultCode -> typed exception
# ---------------------------------------------------------------------------
# Codes not yet exposed by the PAC are omitted; they will fall through to
# AerospikeError until the PAC adds them.
_RC_TO_TYPE: dict[ResultCode, type[AerospikeError]] = {
ResultCode.GENERATION_ERROR: GenerationError,
# Authentication
ResultCode.NOT_AUTHENTICATED: AuthenticationError,
ResultCode.INVALID_USER: AuthenticationError,
# Security (catch-all for remaining security codes)
ResultCode.ILLEGAL_STATE: SecurityError,
ResultCode.USER_ALREADY_EXISTS: SecurityError,
ResultCode.FORBIDDEN_PASSWORD: SecurityError,
ResultCode.SECURITY_NOT_SUPPORTED: SecurityError,
ResultCode.SECURITY_NOT_ENABLED: SecurityError,
ResultCode.SECURITY_SCHEME_NOT_SUPPORTED: SecurityError,
# Timeout
ResultCode.TIMEOUT: TimeoutError,
ResultCode.QUERY_TIMEOUT: TimeoutError,
# Namespace
ResultCode.INVALID_NAMESPACE: InvalidNamespaceError,
# Query terminated
ResultCode.QUERY_ABORTED: QueryTerminatedError,
}
def _result_code_to_exception(
result_code: ResultCode,
message: str = "",
in_doubt: bool = False,
) -> AerospikeError:
"""Map a ``ResultCode`` to the appropriate typed exception.
Map a server result code to the appropriate typed exception.
"""
cls = _RC_TO_TYPE.get(result_code, AerospikeError)
return cls(message, result_code=result_code, in_doubt=in_doubt)
# ---------------------------------------------------------------------------
# Boundary converter: PAC exception -> PFC exception
# ---------------------------------------------------------------------------
def _convert_pac_exception(exc: Exception) -> AerospikeError:
"""Convert a PAC exception to the appropriate PFC typed exception.
The original exception is **not** set as ``__cause__`` here; callers
should use ``raise convert_pac_exception(e) from e``.
:func:`_result_code_to_exception`
"""
if isinstance(exc, PacServerError):
return _result_code_to_exception(exc.result_code, str(exc), exc.in_doubt)
if isinstance(exc, PacTimeoutError):
return TimeoutError(str(exc))
if isinstance(exc, PacConnectionError):
return ConnectionError(str(exc))
if isinstance(exc, PacInvalidNodeError):
return InvalidNodeError(str(exc))
if isinstance(exc, PacUDFBadResponse):
return _result_code_to_exception(
ResultCode.UDF_BAD_RESPONSE,
str(exc),
getattr(exc, "in_doubt", False),
)
if isinstance(exc, PacAerospikeError):
return AerospikeError(str(exc))
return AerospikeError(str(exc))