Error Handling & Retries
Relevant source files
The following files were used as context for generating this wiki page:
The bubus project implements a robust error handling and retry system designed to provide fault tolerance for event-driven architectures. The system distinguishes between event-level failures (such as hard timeouts) and handler-level failures, ensuring that a single failing handler does not block the entire event bus or prevent other handlers from executing.
Central to this architecture is a unified error taxonomy and a specialized retry mechanism that supports exponential backoff, concurrency control via semaphores, and per-attempt timeouts. The system is designed for parity between Python and TypeScript runtimes, utilizing context-manager styles and decorators to provide a clean API for developers.
Sources: TODO_ts.md:12-16, TODO_python.md:13-18, bubus-ts/tests/error_handling.test.ts:13-35
Core Error Taxonomy
The project enforces a specific set of error classes to categorize failures consistently across different execution paths. These errors are used to normalize terminal states in both the TypeScript and Python runtimes.
| Error Class | Description |
|---|---|
EventHandlerCancelledError | Raised when a handler is cancelled before execution begins. |
EventHandlerResultSchemaError | Raised when a handler's return value fails to validate against the event's result schema. |
EventHandlerTimeoutError | Raised when a handler exceeds its allocated execution time. |
EventHandlerAbortedError | Raised when a running handler is forcefully stopped, often due to an event-level hard timeout. |
RetryTimeoutError | Specific to the retry utility; indicates a single attempt timed out. |
SemaphoreTimeoutError | Raised when a retry-wrapped function fails to acquire a concurrency slot within the limit. |
Sources: TODO_ts.md:18-22, TODO_python.md:21-25, bubus-ts/src/retry.ts:46-68
Handler-Level Retry Mechanism
The retry utility provides a configurable higher-order function or decorator to wrap async operations. It manages the lifecycle of multiple attempts, including delays and error filtering.
Retry Logic Flow
The following diagram illustrates the internal logic of the retry wrapper, including backoff and error matching.
This flow ensures that only transient, specified errors trigger a retry, while persistent or unexpected errors terminate execution.
Sources: bubus-ts/src/retry.ts:145-188, bubus-ts/tests/retry.test.ts:18-50
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
max_attempts | number | 1 | Total attempts including the initial call. |
retry_after | number | 0 | Seconds to wait between retries. |
retry_backoff_factor | number | 1.0 | Multiplier for exponential backoff. |
retry_on_errors | Array | undefined | Filter for specific Error classes, names, or Regex. |
timeout | number | null | Per-attempt hard timeout in seconds. |
Sources: bubus-ts/src/retry.ts:7-24
Concurrency and Semaphore Control
The retry system integrates with an AsyncSemaphore to limit concurrent executions of specific handlers. This is particularly useful for rate-limiting external API calls or managing resource-heavy operations.
Semaphore Scoping Strategies
Semaphores can be scoped to different levels of the application:
- Global: All calls to functions sharing the same
semaphore_nameuse the same slot pool. - Class: All instances of a specific class share a slot pool for that method.
- Instance: Each object instance maintains its own independent slot pool.
Sources: bubus-ts/src/retry.ts:31-41, bubus-ts/tests/retry.test.ts:192-228
Deadlock Prevention
To prevent deadlocks in nested retry-wrapped calls, the system implements re-entrancy tracking. In TypeScript, this uses AsyncLocalStorage to track semaphores already held by the current async call stack. If a nested call requests a semaphore it already holds, the acquisition is skipped.
The diagram shows how the wrapper manages the semaphore lifecycle, ensuring slots are released even if errors occur.
Sources: bubus-ts/src/retry.ts:80-101, bubus-ts/tests/retry.test.ts:400-435
Event-Level Error Isolation
A core principle of the EventBus is error isolation. When an event is dispatched, multiple handlers may be triggered. The failure of one handler does not stop the execution of others.
- Result Collection: Every handler execution produces an
EventResult. Errors are captured and stored in theerrorfield of the result, while the overall event status transitions tocompletedregardless of individual handler success. - Event Errors: The
BaseEventobject maintains anevent_errorsarray that aggregates all errors encountered across all handlers for that specific event instance.
Sources: bubus-ts/tests/error_handling.test.ts:13-35, bubus-ts/tests/eventbus_basics.test.ts:460-492
Timeouts
The system implements two layers of timeouts:
- Event Timeout: A hard cap across all handlers for an event. If this expires, all pending handlers are cancelled and started handlers are aborted.
- Handler Timeout: Specific to a single handler. It defaults to the
event_timeoutif unset.
Sources: TODO_ts.md:6-7, TODO_python.md:7-8, bubus-ts/src/retry.ts:121-125
Timeout Interaction with Retries
When a timeout is configured in the retry options, the retry loop treats a timeout as a RetryTimeoutError, which can trigger subsequent retry attempts if max_attempts > 1.
Sources: bubus-ts/src/retry.ts:170-175, bubus-ts/tests/retry.test.ts:145-165
Conclusion
The error handling and retry system in bubus ensures that transient failures are handled gracefully through automated retries and exponential backoff, while concurrency controls prevent system overload. By isolating handler errors and providing a consistent taxonomy, the bus maintains high availability even when individual components fail.