sipeed/picoclaw · warning
events: bus is closed
Error message
events: bus is closed
What it means
Returned by the events.EventBus Subscribe family (bus.go also returns it for a nil *EventBus, and Publish paths surface the analogous condition) when subscribing to a bus that is nil or already closed via Close(). During gateway shutdown this is an expected signal — internal agent code (agent_outbound.go) explicitly treats errors.Is(err, bus.ErrBusClosed) as non-fatal.
Source
Thrown at pkg/events/subscription.go:17
package events
import (
"context"
"errors"
"log"
"runtime/debug"
"sync"
"sync/atomic"
"time"
)
const defaultSubscriberBuffer = 16
var (
// ErrBusClosed is returned when subscribing to a closed event bus.
ErrBusClosed = errors.New("events: bus is closed")
// ErrNilHandler is returned when subscribing without a handler.
ErrNilHandler = errors.New("events: handler is nil")
)
// Handler processes a runtime event delivered to a subscription.
type Handler func(context.Context, Event) error
// SubscribeOptions controls how a subscription receives events.
type SubscribeOptions struct {
Name string
Buffer int
Priority int
Concurrency ConcurrencyKind
Backpressure BackpressurePolicy
// Timeout bounds how long the subscription worker waits for one handler call.
// Handlers should still honor ctx cancellation; timed-out calls keep running
// until their handler returns.
Timeout time.DurationView on GitHub (pinned to 49183d7e8d)
Solutions
- Treat it as a stop signal: check errors.Is(err, events.ErrBusClosed) in subscribe-retry loops and exit the loop instead of retrying
- Create a fresh EventBus for the next lifecycle instead of reusing the closed one
- Fix shutdown ordering: stop/quiesce subscriber components before calling bus.Close()
- If it appears on startup, the bus pointer is nil — initialize it before passing it to components
Example fix
// before
for {
sub, err := ch.Subscribe(ctx, opts, h)
if err != nil { continue } // spins forever after shutdown
}
// after
sub, err := ch.Subscribe(ctx, opts, h)
if err != nil {
if errors.Is(err, events.ErrBusClosed) { return } // expected during shutdown
return err
} Defensive patterns
Strategy: try-catch
Type guard
func isBusClosed(err error) bool {
return errors.Is(err, events.ErrBusClosed)
} Try / catch
sub, err := ch.Subscribe(ctx, opts, handler)
if err != nil {
if errors.Is(err, events.ErrBusClosed) {
return nil // expected during shutdown: stop subscribing, unwind
}
return err // real failure: log and apply backoff/retry
} Prevention
- Never reuse a bus after Close(); create a new EventBus per lifecycle
- Close subscriber components before closing the bus during shutdown
- Track a 'shutting down' flag so re-subscribe loops stop before touching the closed bus
When it happens
Trigger: Calling Subscribe/SubscribeOnce on a *EventBus that is nil, or after bus.Close() has completed; a re-subscribe loop racing shutdown ordering (bus closed while a component tries to reconnect).
Common situations: Shutdown races where a subscriber goroutine re-subscribes after the gateway closed the bus; storing a bus pointer that was never initialized (nil receiver path in bus.go:173); reusing a bus instance across test runs without recreating it.
Related errors
- process hook %q is closed
- channel stopped during QR setup
- events: handler is nil
- invalid pid file
- session already completed
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/63d5c58a736cd227.
Report an issue: GitHub.