Common Mistakes

New to HORUS? Here are the most common mistakes beginners make and how to fix them.


1. Using Slashes in Topic Names

The Problem:

// Works on Linux only — fails on macOS!
let topic: Topic<f32> = Topic::new("sensors/lidar")?;

Why: On Linux, slashes create subdirectories under /dev/shm/horus/topics/ which works fine. On macOS, shm_open() does not support slashes in names, so this will fail.

The Fix:

// CORRECT — Use dots for cross-platform compatibility
let topic: Topic<f32> = Topic::new("sensors.lidar")?;

Use dot-separated names ("sensors.lidar", "camera.rgb") for portable topic names that work on all platforms.


2. Forgetting to Call recv() Every Tick

The Problem:

fn tick(&mut self) {
    // Only check for messages sometimes
    if self.counter % 10 == 0 {
        if let Some(data) = self.sensor_sub.recv() {
            self.process(data);
        }
    }
    self.counter += 1;
}

Why: Messages can be missed if you don't check every tick. Topic uses a ring buffer (16-1024 slots by default), and old messages are overwritten when the buffer fills up.

The Fix:

fn tick(&mut self) {
    // ALWAYS check for new messages
    if let Some(data) = self.sensor_sub.recv() {
        self.last_data = Some(data);
    }

    // Use cached data for processing
    if self.counter % 10 == 0 {
        if let Some(ref data) = self.last_data {
            self.process(data);
        }
    }
    self.counter += 1;
}

3. Blocking in tick()

The Problem:

fn tick(&mut self) {
    // WRONG - This blocks the entire scheduler!
    let data = std::fs::read_to_string("large_file.txt").unwrap();
    std::thread::sleep(Duration::from_millis(100));
}

Why: All nodes run in a single tick cycle. Blocking one node blocks them all.

The Fix:

fn init(&mut self) -> Result<()> {
    // Do slow initialization in init(), not tick()
    self.data = std::fs::read_to_string("large_file.txt")?;
    Ok(())
}

fn tick(&mut self) {
    // Keep tick() fast - ideally under 1ms
    self.process(&self.data);
}

4. Wrong Priority Order

The Problem:

// WRONG - Logger runs before sensor!
scheduler.add(logger).order(0).build()?;       // Order 0 (runs first)
scheduler.add(sensor).order(10).build()?;      // Order 10
scheduler.add(controller).order(5).build()?;   // Order 5

Why: Lower order number = runs first. Safety-critical code should be order 0.

The Fix:

// CORRECT - Proper ordering
scheduler.add(safety_monitor).order(0).build()?;   // Safety first!
scheduler.add(sensor).order(5).build()?;           // Then sensors
scheduler.add(controller).order(10).build()?;      // Then control
scheduler.add(logger).order(100).build()?;         // Logging last

5. Not Implementing shutdown() for Motors

The Problem:

impl Node for MotorController {
    fn name(&self) -> &str { "motor" }

    fn tick(&mut self) {
        self.motor.set_velocity(self.velocity);
    }

    // No shutdown() implemented!
}

Why: When you press Ctrl+C, the motor keeps running at its last velocity!

The Fix:

impl Node for MotorController {
    fn name(&self) -> &str { "motor" }

    fn tick(&mut self) {
        self.motor.set_velocity(self.velocity);
    }

    fn shutdown(&mut self) -> Result<()> {
        // CRITICAL: Stop motor on shutdown!
        hlog!(info, "Stopping motor for safe shutdown");
        self.motor.set_velocity(0.0);
        Ok(())
    }
}

6. Not Deriving Required Traits for Custom Messages

The Problem:

struct MyMessage {
    x: f32,
    y: f32,
}

// Error: the trait bound `MyMessage: Clone` is not satisfied
let topic: Topic<MyMessage> = Topic::new("data")?;

Why: Topic requires types to implement Clone, Serialize, and Deserialize.

The Fix:

use serde::{Serialize, Deserialize};

#[derive(Clone, Serialize, Deserialize)]
struct MyMessage {
    x: f32,
    y: f32,
    name: String,  // Strings work fine!
    data: Vec<f32>,  // Vecs work too!
}

let topic: Topic<MyMessage> = Topic::new("data")?;

Or use the standard message types which already have the required traits:

use horus::prelude::*;

let topic: Topic<CmdVel> = Topic::new("cmd_vel")?;
let topic: Topic<Odometry> = Topic::new("odom")?;

7. Thinking send() Returns a Result

The Problem:

fn tick(&mut self) {
    // WRONG - send() is infallible, this won't compile
    if let Err(e) = self.pub_topic.send(data) {
        hlog!(warn, "Failed to publish: {:?}", e);
    }
}

Why: send() returns (), not Result. It uses ring buffer "keep last" semantics — when the buffer is full, the oldest message is overwritten. This means send() always succeeds.

The Fix:

fn tick(&mut self) {
    // CORRECT - send() is infallible, just call it
    self.pub_topic.send(data);
}

8. Creating Topic Inside tick()

The Problem:

fn tick(&mut self) {
    // WRONG - Creates new Topic every tick!
    let topic: Topic<f32> = Topic::new("data").unwrap();
    topic.send(42.0);
}

Why: Creating a Topic is expensive (opens shared memory). Doing it every tick wastes resources.

The Fix:

struct MyNode {
    topic: Topic<f32>,  // Store Topic in struct
}

impl MyNode {
    fn new() -> Result<Self> {
        Ok(Self {
            topic: Topic::new("data")?,  // Create once
        })
    }
}

fn tick(&mut self) {
    self.topic.send(42.0);  // Reuse existing Topic
}

9. Mismatched Topic Types

The Problem:

// Publisher sends f32
let pub_topic: Topic<f32> = Topic::new("data")?;
pub_topic.send(42.0);

// Subscriber expects i32
let sub_topic: Topic<i32> = Topic::new("data")?;  // WRONG TYPE!
let value = sub_topic.recv();  // Will get garbage data

Why: HORUS doesn't check types at runtime. Mismatched types cause data corruption.

The Fix:

// Use the SAME type for publisher and subscriber
let pub_topic: Topic<f32> = Topic::new("data")?;
let sub_topic: Topic<f32> = Topic::new("data")?;  // Same type!

Pro tip: Use named message types to avoid confusion:

type SensorReading = f32;
let pub_topic: Topic<SensorReading> = Topic::new("sensor")?;
let sub_topic: Topic<SensorReading> = Topic::new("sensor")?;

10. Using Raw Node Trait When node! Macro Would Be Simpler

The Problem:

// Manual implementation - lots of boilerplate
struct MySensor {
    pub_topic: Topic<f32>,
}

impl MySensor {
    fn new() -> Result<Self> {
        Ok(Self {
            pub_topic: Topic::new("sensor.data")?,
        })
    }
}

impl Node for MySensor {
    fn name(&self) -> &str { "MySensor" }

    fn tick(&mut self) {
        let data = 42.0;  // Read sensor
        self.pub_topic.send(data);
    }
}

The Fix:

// Use node! macro - 75% less code!
node! {
    MySensor {
        pub { sensor_data: f32 -> "sensor.data" }

        tick {
            let data = 42.0;  // Read sensor
            self.sensor_data.send(data);
        }
    }
}

See node! Macro for more examples.


Quick Reference

MistakeFix
Slashes in topic namesUse dots: sensors.lidar
Not checking recv() every tickAlways call recv(), cache last value
Blocking in tick()Keep tick() under 1ms, do I/O in init()
Wrong priority orderLower number = higher priority
No shutdown() for motorsAlways stop actuators in shutdown()
Missing derives on messagesAdd Clone, Serialize, Deserialize
Treating send() as falliblesend() is infallible — just call it directly
Creating Topic in tick()Create Topic once in new()
Mismatched topic typesUse same type for pub and sub
Too much boilerplateUse the node! macro

Still Having Issues?

  • Check Troubleshooting for error messages
  • See Examples for working code
  • Run horus monitor to see what your nodes are doing