Skip to content

baldur.core.exceptions — Exception Hierarchy

Top-level re-export selection rule: a name is exported by baldur/__init__.py iff it is (a) a domain base class, or (b) a leaf class raised by code reachable from a top-level public surface. Everything else stays in baldur.core.exceptions and is reachable via the nested import path.

When-to-catch (13 top-level exceptions)

Exception When raised Recovery hint
BaldurError Catch-all base Last-resort logging + alert
AdapterError Adapter base (Redis/SQL misuse) Reconnect; fall back to in-memory
AdapterNotFoundError a ProviderRegistry slot's get(...) finds no provider by that name Wire the missing adapter
CircuitBreakerError CB-base — domain-specific subclasses Do NOT retry — non-retryable
DLQError DLQ-base Inspect DLQ entry; manual replay if needed
DLQReplayError retrying a single DLQ entry (retry_entry) could not complete Mark for manual review
ResilienceError Resilience-pattern base Domain-specific fallback
RetryExhaustedError All retry attempts failed Send to DLQ; alert on SLA breach
TimeoutPolicyError protect(timeout=...) exceeded Cancel downstream; degrade
RateLimitExceeded @rate_limit rejected the call Surface 429; honor reset_at
IdempotencyDuplicateError @idempotent or idempotency_key= detected a duplicate Treat as "already happened" — no result is returned (decision: SKIP completed, ABORT still running); do NOT retry
DomainValidationError Domain-tag validation rejected the call Caller bug — fix the domain identifier
ConfigurationError Settings or wiring misconfiguration Crash fast — operator must fix env

Nested-only classes

exceptions

Baldur library exception hierarchy.

All library exceptions inherit from BaldurError. Callers can use except BaldurError to catch any library error.

Top-level re-export selection rule

A name in core/exceptions.__all__ is re-exported by baldur/__init__.py iff it is either (a) a domain base class, or (b) a leaf class raised by code reachable from a top-level public surface (protect, decorators, ...).

Re-exported at baldur top-level (15 names): Bases — BaldurError, AdapterError, CircuitBreakerError, DLQError, ResilienceError, ConfigurationError Leaves — AdapterNotFoundError, RetryExhaustedError, TimeoutPolicyError, RateLimitExceeded, IdempotencyDuplicateError, IdempotencyUnavailableError, DomainValidationError, DLQReplayError, LLMUnavailableError

Internal / nested-only (baldur.core.exceptions): AdapterInitializationError, AdapterConnectionError, RecoveryAdapterError, StoreError, UnconfiguredStoreError, CircuitBreakerTransitionError, InvalidStateTransitionError, DLQEntryNotFoundError, AuditError, DistributedHashChainUnavailableError, HashChainSequenceRefusedError, RunbookError, SettingsValidationError, StepExecutionError, StepTimeoutError, CompensationError, ConcurrencyConflictError, RateLimitDeferredError (re-exported by baldur.services.rate_limit_coordinator).

BaldurError

BaldurError(message: str = '', *, code: str = '')

Bases: Exception

Base exception for all baldur library errors.

extra_context

extra_context() -> dict[str, Any]

Return structlog-bindable context. Override in subclasses.

AdapterError

AdapterError(message: str = '', *, code: str = '')

Bases: BaldurError

Base exception for adapter-related errors.

AdapterNotFoundError

AdapterNotFoundError(
    message: str = "",
    *,
    adapter_type: str = "",
    adapter_name: str = "",
    code: str = ""
)

Bases: AdapterError

Raised when a requested adapter is not registered in ProviderRegistry.

AdapterInitializationError

AdapterInitializationError(
    message: str = "", *, code: str = ""
)

Bases: AdapterError

Raised when an adapter fails to initialize.

AdapterConnectionError

AdapterConnectionError(
    message: str = "", *, code: str = ""
)

Bases: AdapterError

Raised when an external system connection fails.

RecoveryAdapterError

RecoveryAdapterError(
    message: str = "",
    *,
    service_name: str = "",
    replicas: int | None = None,
    namespace: str = "",
    code: str = ""
)

Bases: AdapterError

Raised when the recovery adapter encounters an error.

Used for input validation and workload detection failures.

StoreError

StoreError(message: str = '', *, code: str = '')

Bases: AdapterError

Base exception for domain state store errors.

Used by domain-specific stores (ConfigHistoryStore, CanaryRolloutStore, ChaosExperimentStore, CrossClusterStore) for data corruption or store-level logic errors.

Infrastructure failures (Redis down) are handled transparently by ResilientStorageBackend's silent fallback — this exception covers cases where the store itself encounters an unrecoverable problem (e.g. unparseable data, schema mismatch).

UnconfiguredStoreError

UnconfiguredStoreError(
    message: str = "",
    *,
    service: str = "",
    operation: str = ""
)

Bases: AdapterError

Raised when a store declines to reach for a Redis nobody configured.

Not a failure report — no connection was attempted. A store path whose only fallback lives in its caller raises this instead of returning a value that would read as a verdict from the store, so the caller takes its local path knowing the store never answered.

Distinct from AdapterConnectionError, which reports a dial that failed. This one says the dial was declined: the address came from the shipped default through every channel an operator could have used, and this process has never reached a Redis there. A store someone named keeps its ordinary loud failure.

CircuitBreakerError

CircuitBreakerError(message: str = '', *, code: str = '')

Bases: BaldurError

Base exception for circuit breaker errors.

CircuitBreakerTransitionError

CircuitBreakerTransitionError(
    message: str = "", *, code: str = ""
)

Bases: CircuitBreakerError

Raised when a circuit breaker state transition fails.

InvalidStateTransitionError

InvalidStateTransitionError(
    message: str = "",
    *,
    current: str = "",
    target: str = "",
    entity_id: str = "",
    code: str = ""
)

Bases: BaldurError

Raised when an invalid state transition is attempted.

Follows the same pattern as CircuitBreakerTransitionError but is domain-agnostic (Recovery Session, etc.).

DLQError

DLQError(message: str = '', *, code: str = '')

Bases: BaldurError

Base exception for DLQ (Dead Letter Queue) errors.

DLQEntryNotFoundError

DLQEntryNotFoundError(message: str = '', *, code: str = '')

Bases: DLQError

Raised when a DLQ entry is not found.

DLQStateConflictError

DLQStateConflictError(message: str = '', *, code: str = '')

Bases: DLQError

Raised when a DLQ operation violates an entry's state precondition.

Covers resolved/archived/at-cap/not-in-replayable-state conflicts (e.g. a double-click force-redrive, or a retry of an already-resolved entry). Maps to HTTP 409 Conflict at the handler layer, distinct from a not-found (404) or an unexpected replay-execution failure (500).

DLQReplayError

DLQReplayError(message: str = '', *, code: str = '')

Bases: DLQError

Raised when a DLQ replay operation fails.

ResilienceError

ResilienceError(message: str = '', *, code: str = '')

Bases: BaldurError

Base exception for resilience pattern errors (bulkhead, retry, and related).

RetryExhaustedError

RetryExhaustedError(message: str = '', *, code: str = '')

Bases: ResilienceError

Raised when all retry attempts are exhausted.

TimeoutPolicyError

TimeoutPolicyError(
    timeout_seconds: float, message: str = ""
)

Bases: ResilienceError

Raised when a protected call exceeds its timeout budget.

RateLimitExceeded

RateLimitExceeded(
    message: str = "",
    *,
    key: str = "",
    limit: int = 0,
    window_seconds: int = 0,
    reset_at: int = 0
)

Bases: ResilienceError

Raised by @rate_limit when a call is rejected by the limiter.

Function-level rejection signal — distinct from RateLimitStorageError (storage-backend failure).

RateLimitDeferredError

RateLimitDeferredError(
    message: str = "",
    *,
    key: str = "",
    not_before: float | None = None
)

Bases: ResilienceError

Raised when an outbound 429 cooldown outlasts the caller's wait budget.

Outbound cooldown deferral signal — distinct from RateLimitExceeded (inbound limiter rejection) and RateLimitStorageError (storage-backend failure).

The call was never attempted: retrying it at or after not_before is safe, which makes this signal suitable for a DLQ/scheduler requeue. It is in the default non-retryable set for the same reason — a retry loop that re-called before not_before would only be refused again.

LLMUnavailableError

LLMUnavailableError(
    message: str = "",
    *,
    attempts: tuple[tuple[str, str], ...] = ()
)

Bases: ResilienceError

Raised by a baldur.llm.wrap client when no endpoint answered the call.

Every endpoint was tried in order — each with its own waits, retries and breaker — and each was rate-limited, overloaded, out of quota, refused the key, failed, or stood behind an open breaker. The last endpoint's error is chained as __cause__.

It is the one error a job sees for "the provider could not answer right now": under @protected(..., dlq=True) it is parked under the job's name, and with replay=True the job is re-run when its breaker closes. It is not retried by an enclosing retry stage: every endpoint already ran its own retries, so running them all again would only multiply calls to providers that are refusing them.

Attributes:

Name Type Description
attempts

(endpoint name, category) for every endpoint tried, in order — the category is the provider's answer (rate_limited, overloaded, quota_exhausted, auth_failed, transient) or what stopped the call before it was sent (breaker_open, rate_limit_deferred, deadline).

IdempotencyDuplicateError

IdempotencyDuplicateError(
    message: str = "",
    *,
    key: str = "",
    domain: str = "",
    decision: str = ""
)

Bases: BaldurError

Raised by @idempotent on a detected duplicate or in-flight collision.

Inherits BaldurError directly (correctness contract, not a resilience stage). Non-retryable by default — outer @dlq_protect layers should treat this as a terminal signal.

IdempotencyUnavailableError

IdempotencyUnavailableError(
    message: str = "", *, key: str = "", error: str = ""
)

Bases: BaldurError

Raised when an idempotency check cannot complete due to a cache I/O failure (e.g. Redis unreachable) on an enabled, explicitly-requested gate.

Distinct from IdempotencyDuplicateError (a successful dedup verdict): this signals the verdict is unknown, so the caller can assume neither "safe to skip" nor "safe to run". Fail-closed by default — opt into fail-open via BALDUR_IDEMPOTENCY_FAIL_OPEN_ON_CACHE_ERROR or the per-call idempotency_fail_open=True. Wraps the original cache exception (raised from it) so a backend-specific error never leaks across the boundary.

DomainValidationError

DomainValidationError(
    message: str = "",
    *,
    original_domain: str = "",
    reason: Any = None
)

Bases: BaldurError

Raised when a domain input string fails validation.

Carries the original (pre-normalization) input and a typed reject reason for downstream logging / metric labelling.

Modeled on RecoveryAdapterError: raised at validation sites that have a loud failure mode (decoration-time, where a CI/dev surface can recover via test or rename). Runtime APIs catch this and fall back to FALLBACK_DOMAIN instead of propagating.

AuditError

AuditError(message: str = '', *, code: str = '')

Bases: BaldurError

Base exception for audit-related errors (cascade, WAL, mmap buffer).

DistributedHashChainUnavailableError

DistributedHashChainUnavailableError(
    message: str = "", *, redis_url: str = ""
)

Bases: AuditError

Raised when a stated distributed audit hash chain cannot be built.

The operator asked for the cross-host chain and no Redis client could be constructed at all. Falling back to the local file-locked chain here would write entries indistinguishable from a chain nobody asked to be distributed — a cross-host guarantee silently served by a mechanism that cannot span hosts.

Only the unlabelled substitution refuses. A client that builds but whose server is unreachable keeps writing, because every entry its fallback produces is stamped degraded and an auditor can tell those apart.

HashChainSequenceRefusedError

HashChainSequenceRefusedError(
    message: str = "",
    *,
    manager: str = "",
    log_dir: str = "",
    filename_pattern: str = "",
    error: str = ""
)

Bases: AuditError

Raised when the chain cannot read the ledger it is about to append to.

The sequence a chain mints is only safe against re-use if the entries already on disk can be seen. When the ledger cannot be read at all — the directory is unreadable, a file read fails, a single row is larger than the tail window, or the walk budget ran out — minting anyway would place a number the ledger may already hold, and returning "fresh ledger" would place 1 inside a live one.

Raised before any mutation of the sequence source, so a refusal consumes no number and leaves nothing to abort. The caller's write fails the same way a failed file open already does: logged, re-raised, and held by the recovery-replay sync worker rather than silently dropped.

RunbookError

RunbookError(message: str = '', *, code: str = '')

Bases: BaldurError

Base exception for runbook errors.

ConfigurationError

ConfigurationError(message: str = '', *, code: str = '')

Bases: BaldurError

Base exception for configuration and settings errors.

SettingsValidationError

SettingsValidationError(
    message: str = "", *, code: str = ""
)

Bases: ConfigurationError

Raised when settings validation fails.

StepExecutionError

StepExecutionError(message: str = '', *, code: str = '')

Bases: BaldurError

Base exception for step execution engine errors.

Shared by the step-based execution engines. Domain-specific subclasses live in each service module.

StepTimeoutError

StepTimeoutError(
    step_type: str | float = "",
    timeout_seconds: float | int = 0,
    message: str = "",
)

Bases: StepExecutionError

Step execution timed out.

Raised by TimeoutExecutor when a handler does not complete within timeout_seconds.

Supports two call conventions

StepTimeoutError(step_type="X", timeout_seconds=30) # keyword StepTimeoutError("X", 30) # positional (legacy compat) StepTimeoutError(timeout_seconds=30) # timeout only (TimeoutExecutor)

CompensationError

CompensationError(message: str = '', *, code: str = '')

Bases: StepExecutionError

Raised when step compensation fails.

Domain-specific subclasses add service-specific extra_context.

ConcurrencyConflictError

ConcurrencyConflictError(
    message: str = "",
    *,
    entity_id: str = "",
    expected_version: int = 0,
    actual_version: int = 0
)

Bases: BaldurError

Raised on optimistic concurrency control (OCC) conflicts.

Covers version CAS failures across the step-execution engines.

ConfigVersionConflictError

ConfigVersionConflictError(
    config_type: str = "",
    *,
    expected_version: int = 0,
    actual_version: int = 0,
    message: str = ""
)

Bases: ConcurrencyConflictError

Raised when a runtime-config write conflicts with a concurrent version.

An optimistic-lock consumer for runtime-config writes, it carries the config section (config_type) plus the inherited expected_version / actual_version so the REST layer can map it to HTTP 409 and hand the client a usable retry token. Lives in OSS core so the OSS config REST handler can except it across the PRO-manager → OSS-handler boundary.

SystemControlStoreError

SystemControlStoreError(
    message: str = "",
    *,
    change: str = "",
    persisted: bool | None = False,
    applies: str = "none",
    withdrew_held_change: bool = False,
    may_still_land: bool = False
)

Bases: BaldurError

Raised when an operator control change did not reach the shared store.

Covers the kill switch, dry-run and emergency-level changes. persisted is False when the store does not hold the change and never will, and None when the write may or may not have landed (a later read of the store decides it). applies says where the change is in force: "none" (nowhere) or "this_process" (held locally). Lives in OSS core so the OSS REST handlers can map it to HTTP 503 across the PRO-manager boundary.

non_retryable_exceptions

non_retryable_exceptions() -> tuple[type[Exception], ...]

Exceptions that must never be retried.

CircuitBreakerError: CB OPEN means 'stop sending traffic'. Retrying defeats circuit breaker semantics. Industry standard (Hystrix, Resilience4j, Polly).

RateLimitDeferredError: the call was never attempted because a shared 429 cooldown outlasts the caller's wait budget. A loop that retries it can only sleep through attempts that cannot succeed before not_before — the refusal is a scheduling signal, not a transient failure.

LLMUnavailableError: a wrapped LLM client already tried every endpoint, each with its own retries. Retrying the whole call would multiply those attempts against providers that are refusing them.