MESH ONLINECODENAME: Purple Rain

Error Codes

This page enumerates every error type the core crate surfaces. Errors are organized by the operation they come from — ingestion, consumption, and adapter — and each variant includes the conditions under which it fires and the right response from a caller.

The crate uses thiserror throughout, so every variant has a Display impl and (where it wraps another error) a working source() chain. Pattern match on the variant when you need to make a decision; format the Display when you need to log.

IngestionError

Returned from EventBus::ingest() and EventBus::ingest_raw().

VariantDisplayWhen it firesWhat to do
Backpressure"backpressure: ring buffer full"Shard's ring buffer full + backpressure policy rejected the eventApply your retry policy; the bus will not surface this if Block mode
Sampled"event dropped due to sampling"Sampling/decimation policy dropped the event before it reached a shardExpected under sampling; no caller action needed
Unrouted"event has no routable shard"Hashed shard id is not in the routing table (e.g. mid-scaling)Back off briefly and retry — topology stabilizes within milliseconds
ShuttingDown"event bus is shutting down"Bus is in shutdown; new ingests rejectedStop ingesting; flush downstream state and exit
Serialization(_)"serialization error: ..."Event payload couldn't be serializedBug — investigate the payload; the error's source chain points at the underlying serde_json::Error

Unrouted is distinct from Backpressure so callers can apply the right remediation. Backpressure says "the destination is full"; unrouted says "there's no destination right now." Pre-fix versions of the bus collapsed these into one variant, and callers applied back-off-and-retry to unrouted errors that wouldn't be fixed by waiting — they needed to retry until the topology settled, which is a different shape of retry.

ConsumerError

Returned from EventBus::poll().

VariantDisplayWhen it firesWhat to do
Adapter(_)"adapter error: ..."Underlying adapter failed; the wrapped error is the adapter'sSee AdapterError below; is_retryable() says whether to retry
InvalidCursor(_)"invalid cursor: ..."Cursor in the request couldn't be decodedDon't pass that cursor again; start from current tail with no cursor
InvalidFilter(_)"invalid filter: ..."Filter in the request couldn't be parsed or evaluatedBug — investigate the filter; the message includes a parse position

A ConsumerError::Adapter wraps an AdapterError, so the full classification surface is available through the wrapped error. Use From<AdapterError> to convert, or pattern match on the wrapper.

AdapterError

Returned from adapter operations (Adapter::on_batch, Adapter::poll_shard, Adapter::flush, Adapter::shutdown). Also wrapped in ConsumerError.

VariantDisplayWhen it firesClassification
Transient(_)"transient error: ..."Retryable failure (timeout, transient network issue)is_retryable() == true
Fatal(_)"fatal error: ..."Unrecoverable stateis_fatal() == true
Backpressure"backend backpressure"Backend rejected for capacity reasons (Redis MAXLEN, JetStream MaxBytes, etc.)is_retryable() == true
Connection(_)"connection error: ..."Connection-level failure (refused, broken, reset)Not retryable by default — covers both transient ("send failed") and permanent ("not initialized") cases without distinguishing
Shutdown"adapter is shut down"Adapter was asked to stop and is no longer accepting workis_shutdown() == true; distinct from Connection so callers can tell "we asked it to stop" from "transport failure"
Serialization(_)"serialization error: ..."Adapter couldn't serialize/deserialize event dataNot retryable; bug in payload or adapter codec

Classification methods

code
impl AdapterError {
    pub fn is_retryable(&self) -> bool;
    pub fn is_fatal(&self) -> bool;
    pub fn is_shutdown(&self) -> bool;
}

The bus's dispatch loop reads these to decide what to do with a failed batch:

  • Retryable. The batch is requeued with an exponential backoff up to a bounded number of attempts.
  • Fatal. The batch is dropped, the bus's stats record the drop, and the error is logged at error level.
  • Shutdown. The batch is dropped and ingestion is halted; the bus's shutdown is presumed to be in flight.
  • Connection (default). Conservatively non-retryable. The bus skips the retry loop and drops the batch immediately. This avoids burning the retry budget on a backend that's gone for good.

The default decision for Connection errors is conservative on purpose. If you know your backend's connection errors are transient and you want them retried, return AdapterError::Transient(...) from your adapter instead.

Subsystem-specific errors

Beyond the core trio, individual subsystems define their own error types. The ones most likely to surface in application code:

ScalingError

Returned from EventBus::add_shards() and EventBus::remove_shards().

VariantWhen it fires
AlreadyAtLimitThe bus is at its configured shard ceiling and can't add more
ShardInUseA shard requested for removal still has in-flight work
NoSuchShardA shard id passed for removal doesn't exist
Internal(_)Internal invariant violation; investigate

ConfigError

Returned from EventBusConfigBuilder::build().

VariantWhen it fires
InvalidShardCountshards outside the supported range
InvalidBatchConfigBatch sizing values inconsistent (max_events == 0, etc.)
IncompatibleFeaturesA feature was requested that isn't compiled in (e.g. redis adapter without the feature)

Adapter-specific errors

Each shipped adapter has its own error type with backend-specific variants. The most useful ones from each:

  • NetAdapterNetAdapterError::SessionFailed, NetAdapterError::RoutingFailed, NetAdapterError::AuthRejected.
  • RedisAdapter — wraps redis::RedisError with classification ("retryable" for read timeouts and replica failovers, "fatal" for auth failures).
  • JetStreamAdapter — wraps async-nats::Error with similar classification.

These are surfaced through AdapterError::Connection, AdapterError::Transient, or AdapterError::Fatal as appropriate, so callers don't need to know the specific backend to apply the right policy. Match on the AdapterError variant, not on the inner error, unless you have a backend-specific reason.

TokenError

Returned from the channel-auth token issuance and verification paths in net::adapter::net::identity.

VariantWhen it firesWhat to do
InvalidToken doesn't deserialize or its signature doesn't verifyReject; the credential is forged or corrupted
ExpiredToken's not_after is in the past, modulo the configured clock-skew windowRe-issue from the current holder; tokens are time-bound on purpose
NotYetValidToken's not_before is in the futureWait, or re-issue with an earlier validity window
ScopeInsufficientToken's scope doesn't cover the requested operationRequest a token with the right scope (publish, subscribe, admin, delegate)
ChannelMismatchToken's channel_hash doesn't match the channel being accessedReject; the token is for a different channel
DelegationDepthExhaustedToken has delegation_depth == 0 and is being re-delegatedThe chain has run out of remaining delegation hops
RevokedToken's nonce is in the revocation listRe-issue
RootNotTrustedToken chain doesn't root at any of the channel's token_rootsThe chain is rooted at the wrong issuer; check the channel's configured trust roots
TtlTooLongRequested TTL exceeds the one-year ceiling (MAX_TOKEN_TTL_SECS)Issue inside the bound; the cap is intentional. The SDK's infallible issue_token helper soft-clamps; try_issue returns this error so callers can decide

The TTL ceiling is a hard cap on the auth surface — issuing a token past one year is rejected on the fallible path and clamped on the SDK's infallible path. Long-lived grants need periodic re-issue, which re-checks the issuer's signing key and current policy.

TagMatcherError

Returned from capability-tag matchers when the requested matcher can't be compiled or evaluated.

VariantWhen it firesWhat to do
InvalidPattern(string)Pattern is syntactically malformedFix the pattern
RegexNotBuiltIn { pattern }A TagMatcher::Regex variant was used against a build of the crate compiled without --features regexRebuild with --features regex or use a different matcher kind

The regex Cargo feature is off by default — regex matching adds about 1.1 MiB to binding artifacts, and most callers don't need it. Builds that do can opt in. Pre-v0.24 the regex-less fallback silently returned empty matches, which made misconfigured queries look indistinguishable from "no entries match"; v0.24 replaced that with the structured error above.

nRPC errors (RpcError, RpcAppError)

Returned from call_typed, call_streaming_typed, call_client_stream_typed, call_duplex_typed, and the underlying MeshRpc surface.

VariantWhen it fires
RpcError::NoServerNo node is currently serving this service name
RpcError::NoMatchingServerA net-where: predicate ruled every advertising server out
RpcError::TimeoutThe call exceeded the configured timeout
RpcError::CanceledA Mesh::cancel(token) aborted the in-flight call
RpcError::PanicThe handler panicked; caught and surfaced typed
RpcError::CodecRequest or response failed to encode/decode (sub-classified: CodecEncode, CodecDecode)
RpcAppError(code, detail)Handler returned a typed application error

The codec error sub-classification is used by every binding's typed wrapper to surface the failure as a binding-native error type (TS / Python / Go all have idiomatic equivalents). The RpcAppError shape is wire-stable across languages — codes like NRPC_TYPED_BAD_REQUEST and NRPC_TYPED_HANDLER_ERROR are part of the cross-language fixture.

Reliable-stream errors (StreamError)

Returned from the reliable-stream API on MeshNode.

VariantWhen it fires
StreamError::WindowFullTx-credit window is exhausted; send_with_retry handles this automatically
StreamError::BackpressureScheduler queue is full for a scheduled stream
StreamError::ClosedStream is closed locally or by the peer
StreamError::ResetPeer sent a SUBPROTOCOL_STREAM_RESET after exhausting retransmit retries; payload includes the reason. Blob-transfer and other consumers map this to a higher-level error promptly instead of stalling to the caller's timeout
StreamError::TimeoutStream operation exceeded its configured timeout

A note on credentials in URLs

Adapter constructors and Debug impls scrub user:password@ from connection URLs before logging or rendering. A misconfigured operator who put a password directly in the URL won't leak it into log sinks — the redactor identifies the rightmost @ in the authority component and replaces the userinfo with [REDACTED].

This is per-adapter behavior, not part of the error API itself, but it shows up in Debug output of every adapter config and is worth knowing about when reading logs.