Error Types

Every error in HORUS is structured and pattern-matchable. Import the short aliases from the prelude:

use horus::prelude::*;

// Result<T> = std::result::Result<T, HorusError>
// Error = HorusError
fn my_function() -> Result<()> {
    let topic: Topic<f32> = Topic::new("sensor")?;
    Ok(())
}

Quick Reference

Prelude NameFull NameWhat it is
Result<T>HorusResult<T>std::result::Result<T, HorusError>
ErrorHorusErrorThe umbrella error enum

Always use Result<T> and Error — never write HorusResult or HorusError directly.

HorusError Variants

HorusError is a #[non_exhaustive] enum with 13 variants. Each wraps a domain-specific sub-error:

VariantSub-error TypeDomainCommon Source
Io(…)std::io::ErrorFile/system I/Ostd::fs::read()
Config(…)ConfigErrorConfigurationhorus.toml parsing
Communication(…)CommunicationErrorIPC, topicsTopic::new()
Node(…)NodeErrorNode lifecycleinit(), tick() panics
Memory(…)MemoryErrorSHM, tensorsImage::new(), pool exhaustion
Serialization(…)SerializationErrorJSON, YAML, TOMLConfig/message parsing
NotFound(…)NotFoundErrorMissing resourcesFrame/topic/node lookup
Resource(…)ResourceErrorResource lifecycleDuplicate names, permissions
InvalidInput(…)ValidationErrorInput validationParameter bounds, format
Parse(…)ParseErrorType parsingInteger, float, bool from strings
InvalidDescriptor(…)StringTensor integrityCross-process tensor validation
Transform(…)TransformErrorCoordinate framesExtrapolation, stale data
Timeout(…)TimeoutErrorTimeoutsService calls, resource waits

Pattern Matching

Match on specific error variants to handle them differently:

use horus::prelude::*;

fn handle_topic_error(err: Error) {
    match err {
        HorusError::Communication(CommunicationError::TopicFull { topic }) => {
            // Back-pressure: subscriber is slower than publisher
            hlog!(warn, "Topic '{}' full, dropping message", topic);
        }
        HorusError::Communication(CommunicationError::TopicNotFound { topic }) => {
            // Topic doesn't exist yet
            hlog!(error, "Topic '{}' not found — is the publisher running?", topic);
        }
        HorusError::Memory(MemoryError::PoolExhausted { reason }) => {
            // Image/PointCloud pool has no free slots
            hlog!(warn, "Pool exhausted: {} — waiting for consumers", reason);
        }
        HorusError::Transform(TransformError::Stale { frame, age, threshold }) => {
            // Transform data is too old
            hlog!(warn, "Frame '{}' stale: {:?} > {:?}", frame, age, threshold);
        }
        other => {
            hlog!(error, "Unexpected error: {}", other);
            // Check for actionable hints
            if let Some(hint) = other.help() {
                hlog!(info, "  hint: {}", hint);
            }
        }
    }
}

Error Hints

Every HorusError has a .help() method that returns an actionable remediation hint:

if let Err(e) = Topic::<f32>::new("sensor") {
    eprintln!("error: {}", e);
    if let Some(hint) = e.help() {
        eprintln!("  hint: {}", hint);
    }
}

Example output:

error: Failed to create topic 'sensor': permission denied
  hint: Check shared memory permissions and available space. Run: horus clean --shm

Severity

Every error has a Severity classification used by the scheduler for automatic recovery:

SeverityMeaningScheduler Action
TransientMay resolve on retry (back-pressure, timeout)Retry
PermanentWon't succeed but system can continueSkip, log warning
FatalData integrity compromised, unrecoverableStop node or scheduler
match err.severity() {
    Severity::Transient => { /* retry */ }
    Severity::Permanent => { hlog!(warn, "{}", err); }
    Severity::Fatal     => { /* emergency stop */ }
}

Adding Context

Use the HorusContext trait to wrap foreign errors with descriptive context:

use horus::prelude::*;

fn load_sensor_config(path: &str) -> Result<String> {
    // .horus_context() wraps std::io::Error with a message
    let data = std::fs::read_to_string(path)
        .horus_context(format!("reading sensor config '{}'", path))?;
    Ok(data)
}

fn load_calibration(path: &str) -> Result<String> {
    // .horus_context_with() is lazy — closure only called on Err
    std::fs::read_to_string(path)
        .horus_context_with(|| format!("reading calibration '{}'", path))
}

The context is preserved in the error chain and displayed as:

reading sensor config 'sensors.yaml'
  Caused by: No such file or directory (os error 2)

Retry Utility

retry_transient automatically retries operations that return transient errors:

use horus::prelude::*;

let config = RetryConfig {
    max_retries: 3,
    base_delay: 100_u64.ms(),
    ..Default::default()
};

let result = retry_transient(&config, || {
    connect_to_sensor()
});

Common Error Scenarios

You're doingError you'll seeWhat to do
Topic::new("name")CommunicationError::TopicCreationFailedCheck SHM permissions, disk space
scheduler.run()NodeError::InitPanicFix the node's init() method
Image::new(w, h, enc)MemoryError::PoolExhaustedConsumers aren't dropping images fast enough
tf.tf("a", "b")TransformError::ExtrapolationIncrease history buffer or check timestamp
client.call(req, timeout)TimeoutErrorServer not running or overloaded
params.set("key", val)ValidationError::OutOfRangeValue outside configured min/max

Sub-Error Details

CommunicationError

TopicFull { topic }              // Ring buffer full
TopicNotFound { topic }          // No such topic
TopicCreationFailed { topic, reason }  // SHM setup failed
NetworkFault { peer, reason }    // Peer unreachable
ActionFailed { reason }          // Action system error

NotFoundError

Frame { name }     // TransformFrame lookup
Topic { name }     // Topic lookup
Node { name }      // Node lookup
Service { name }   // Service lookup
Action { name }    // Action lookup
Parameter { name } // RuntimeParams lookup

ValidationError

OutOfRange { field, min, max, actual }  // Value outside bounds
InvalidFormat { field, expected_format, actual }  // Wrong format
InvalidEnum { field, valid_options, actual }  // Not an allowed value
MissingRequired { field }  // Required field absent
ConstraintViolation { field, constraint }  // Custom constraint
Conflict { field_a, field_b, reason }  // Two fields conflict

ConfigError

ParseFailed { format, reason, source }       // Failed to parse config file
MissingField { field, context }              // Required field missing
ValidationFailed { field, expected, actual } // Value doesn't match constraint
InvalidValue { key, reason }                 // Invalid configuration value
Other(String)                                // General config error

SerializationError

Json { source: serde_json::Error }     // JSON parse/emit failure
Yaml { source: serde_yaml::Error }     // YAML parse/emit failure
Toml { source: toml::ser::Error }      // TOML parse/emit failure
Other { format, reason }               // Other format error

MemoryError

PoolExhausted { reason }        // Tensor pool out of slots
AllocationFailed { reason }     // Memory allocation failed
ShmCreateFailed { path, reason } // Shared memory region creation failed
MmapFailed { reason }           // Memory mapping failed
DLPackImportFailed { reason }   // DLPack tensor import failed
OffsetOverflow                  // Tensor offset exceeds region

NodeError

InitPanic { node }              // Node panicked during init()
ReInitPanic { node }            // Node panicked during re-init
ShutdownPanic { node }          // Node panicked during shutdown()
InitFailed { node, reason }     // init() returned Err
TickFailed { node, reason }     // Tick error
Other { node, message }         // General node error

ResourceError

AlreadyExists { resource_type, name }         // Resource already registered
PermissionDenied { resource, required_permission } // Insufficient permissions
Unsupported { feature, reason }               // Feature not available on this platform

ParseError

Int { input, source: ParseIntError }     // Integer parsing failed
Float { input, source: ParseFloatError } // Float parsing failed
Bool { input, source: ParseBoolError }   // Boolean parsing failed
Custom { type_name, input, reason }      // Custom type parse failed

TransformError

Extrapolation { frame, requested_ns, oldest_ns, newest_ns }  // Out of buffer range
Stale { frame, age, threshold }  // Transform too old

TimeoutError

TimeoutError { resource, elapsed, deadline }  // Operation timed out

Internal & Contextual Variants

Two special variants for framework-internal errors:

// Internal error with source location (use horus_internal!() macro)
Internal { message: String, file: &'static str, line: u32 }

// Error with preserved source chain (use .horus_context() on Results)
Contextual { message: String, source: Box<dyn Error + Send + Sync> }
// Adding context to errors
let device = Device::open("/dev/ttyUSB0")
    .horus_context("opening motor controller serial port")?;

See Also