Skip to content

Error handling

Every exception the driver throws implements Cassandra\Exception. Each one also extends a matching SPL exception, so existing catch (RuntimeException $e) blocks keep working.

php
try {
    $rows = $session->execute($statement, ['arguments' => [$id]]);
} catch (Cassandra\Exception $e) {
    // Any driver error.
}

The hierarchy

Cassandra\Exception  (interface)

├── Cassandra\Exception\LogicException            extends \LogicException
├── Cassandra\Exception\DomainException           extends \DomainException
├── Cassandra\Exception\InvalidArgumentException  extends \InvalidArgumentException
├── Cassandra\Exception\RangeException            extends \RangeException
│   └── Cassandra\Exception\DivideByZeroException

└── Cassandra\Exception\RuntimeException          extends \RuntimeException
    ├── Cassandra\Exception\TimeoutException
    ├── Cassandra\Exception\AuthenticationException
    ├── Cassandra\Exception\ProtocolException

    ├── Cassandra\Exception\ExecutionException
    │   ├── Cassandra\Exception\ReadTimeoutException
    │   ├── Cassandra\Exception\WriteTimeoutException
    │   ├── Cassandra\Exception\UnavailableException
    │   └── Cassandra\Exception\TruncateException

    ├── Cassandra\Exception\ValidationException
    │   ├── Cassandra\Exception\InvalidQueryException
    │   ├── Cassandra\Exception\InvalidSyntaxException
    │   ├── Cassandra\Exception\UnauthorizedException
    │   ├── Cassandra\Exception\UnpreparedException
    │   └── Cassandra\Exception\ConfigurationException
    │       └── Cassandra\Exception\AlreadyExistsException

    └── Cassandra\Exception\ServerException
        ├── Cassandra\Exception\IsBootstrappingException
        └── Cassandra\Exception\OverloadedException

The three branches under RuntimeException split by cause, which is what decides your response:

BranchCauseRetry?
ExecutionExceptionThe cluster could not complete the requestOften yes
ValidationExceptionThe request is wrongNo
ServerExceptionA node is not in a state to serveYes, after a delay

The exceptions that matter

ReadTimeoutException

Not enough replicas answered a read in time. The data may be fine. A retry commonly succeeds, and the default retry policy already tried once.

php
catch (Cassandra\Exception\ReadTimeoutException $e) {
    // Safe to retry. Consider a lower consistency for this specific read.
}

WriteTimeoutException

The coordinator did not get enough acknowledgements in time. The write may have applied. A timeout is not a rejection.

php
catch (Cassandra\Exception\WriteTimeoutException $e) {
    // Retry only when the write is idempotent.
    // A counter increment or a collection append must not be retried blindly.
}

UnavailableException

The coordinator knows there are not enough live replicas to meet the consistency level. It did not even try. Retrying at once fails again.

php
catch (Cassandra\Exception\UnavailableException $e) {
    // A node is down. Back off, or degrade the consistency deliberately.
}

InvalidQueryException and InvalidSyntaxException

A bug in the query, the table name, or the column list. Never retry. Let it reach your error reporting.

AlreadyExistsException

A CREATE for something that exists. Use IF NOT EXISTS in migrations to avoid it.

UnpreparedException

The coordinator does not know the prepared statement identifier, usually because the node restarted. The driver re-prepares and retries on its own. Seeing this in PHP means the recovery also failed.

OverloadedException and IsBootstrappingException

The node is saturated or is still joining the cluster. Back off. Do not retry in a tight loop.

AuthenticationException

Bad credentials. Thrown from connect(). See authentication.

TimeoutException

The client gave up waiting. The request may still be running on the server. This is the PHP-side timeout from execute() or Future::get(), not a server timeout.

InvalidArgumentException

A bad value passed to the driver: an unknown execution option, a negative page size, an unreadable certificate path, a malformed IP address. Thrown before any network work.

DivideByZeroException

From arithmetic on the numeric value classes.

Catch order

Catch from specific to general. PHP takes the first matching block.

php
use Cassandra\Exception\InvalidQueryException;
use Cassandra\Exception\UnavailableException;
use Cassandra\Exception\WriteTimeoutException;

try {
    $session->execute($insert, ['arguments' => $values]);
} catch (WriteTimeoutException $e) {
    $this->queueForVerification($values);          // may or may not have applied
} catch (UnavailableException $e) {
    $this->scheduleRetry($values, delaySeconds: 5); // cluster is degraded
} catch (InvalidQueryException $e) {
    throw $e;                                       // a bug, do not swallow it
} catch (Cassandra\Exception $e) {
    $this->logger->error('cassandra write failed', ['error' => $e->getMessage()]);
    throw $e;
}

What to retry

ExceptionRetryNote
ReadTimeoutExceptionYesReads are idempotent
WriteTimeoutExceptionOnly if idempotentThe write may have applied
UnavailableExceptionAfter a delayRetrying at once fails again
OverloadedExceptionAfter a delayBackoff is required
IsBootstrappingExceptionAfter a delayThe node is joining
TruncateExceptionYes
InvalidQueryExceptionNoFix the query
InvalidSyntaxExceptionNoFix the query
UnauthorizedExceptionNoFix the grant
AlreadyExistsExceptionNoUse IF NOT EXISTS
AuthenticationExceptionNoFix the credentials
InvalidArgumentExceptionNoFix the call

Exception codes

Server-side exceptions carry the C driver error code in getCode(). The message comes from the driver.

php
catch (Cassandra\Exception $e) {
    $this->logger->error('cassandra error', [
        'class'   => $e::class,
        'code'    => $e->getCode(),
        'message' => $e->getMessage(),
    ]);
}

A wrapper for your application

Driver exceptions crossing a domain boundary couple your code to the driver. Translate at the edge:

php
final class UserRepository
{
    public function find(Cassandra\Uuid $id): ?User
    {
        try {
            $row = $this->session
                ->execute($this->findById, ['arguments' => [$id]])
                ->first();
        } catch (Cassandra\Exception\ValidationException $e) {
            throw new StorageBugException($e->getMessage(), previous: $e);
        } catch (Cassandra\Exception $e) {
            throw new StorageUnavailableException($e->getMessage(), previous: $e);
        }

        return $row === null ? null : User::fromRow($row);
    }
}

The split matters. ValidationException is your bug and belongs in the error tracker. Everything else is an infrastructure condition and belongs in a retry or a circuit breaker.

Version 1.4. Released under the Apache License 2.0.