ethereum/go-ethereum · error

channel argument of Subscribe has type %T, need writable cha

Error message

channel argument of Subscribe has type %T, need writable channel

What it means

rpc.Client.Subscribe requires the third argument to be a writable (bidirectional or send-only) Go channel on which notifications are delivered. The method reflects on the argument at setup time and panics if it is not a channel at all, or is receive-only — since the client could never deliver notifications into it.

Source

Thrown at rpc/client.go:508

}

// Subscribe calls the "<namespace>_subscribe" method with the given arguments,
// registering a subscription. Server notifications for the subscription are
// sent to the given channel. The element type of the channel must match the
// expected type of content returned by the subscription.
//
// The context argument cancels the RPC request that sets up the subscription but has no
// effect on the subscription after Subscribe has returned.
//
// Slow subscribers will be dropped eventually. Client buffers up to 20000 notifications
// before considering the subscriber dead. The subscription Err channel will receive
// ErrSubscriptionQueueOverflow. Use a sufficiently large buffer on the channel or ensure
// that the channel usually has at least one reader to prevent this issue.
func (c *Client) Subscribe(ctx context.Context, namespace string, channel interface{}, args ...interface{}) (*ClientSubscription, error) {
	// Check type of channel first.
	chanVal := reflect.ValueOf(channel)
	if chanVal.Kind() != reflect.Chan || chanVal.Type().ChanDir()&reflect.SendDir == 0 {
		panic(fmt.Sprintf("channel argument of Subscribe has type %T, need writable channel", channel))
	}
	if chanVal.IsNil() {
		panic("channel given to Subscribe must not be nil")
	}
	if c.isHTTP {
		return nil, ErrNotificationsUnsupported
	}

	msg, err := c.newMessage(namespace+subscribeMethodSuffix, args...)
	if err != nil {
		return nil, err
	}
	op := &requestOp{
		ids:  []json.RawMessage{msg.ID},
		resp: make(chan []*jsonrpcMessage, 1),
		sub:  newClientSubscription(c, namespace, chanVal),
	}

View on GitHub (pinned to 6bb0588ad8)

Solutions

  1. Allocate a fresh bidirectional channel: ch := make(chan types.Header) and pass ch directly.
  2. Keep the subscription's channel direction as send-capable in your wrapper signatures (use 'chan T', never '<-chan T').
  3. Validate with reflection before calling Subscribe when the channel comes from user/plugin input.

Example fix

// before
var ch <-chan types.Header
sub, err := client.Subscribe(ctx, "eth", ch) // panics

// after
ch := make(chan types.Header)
sub, err := client.Subscribe(ctx, "eth", ch)
defer sub.Unsubscribe()
for h := range ch { fmt.Println(h.Number) }
Defensive patterns

Strategy: type-guard

Validate before calling

func checkWritableChannel(v interface{}) error {
    rv := reflect.ValueOf(v)
    if rv.Kind() != reflect.Chan {
        return fmt.Errorf("need a channel, got %T", v)
    }
    if rv.IsNil() {
        return errors.New("channel must not be nil")
    }
    if rv.Type().ChanDir()&reflect.SendDir == 0 {
        return fmt.Errorf("channel %T is receive-only", v)
    }
    return nil
}
// before Subscribe:
if err := checkWritableChannel(ch); err != nil { return nil, err }

Type guard

func isWritableChannel(v interface{}) bool {
    rv := reflect.ValueOf(v)
    return rv.Kind() == reflect.Chan && !rv.IsNil() &&
        rv.Type().ChanDir()&reflect.SendDir != 0
}

Try / catch

Prefer pre-validation; if wrapping third-party calls, defer/recover the panic and report the offending type from the message 'channel argument of Subscribe has type %T'.

Prevention

When it happens

Trigger: Calling client.Subscribe(ctx, "eth", ch, args...) where ch is a non-channel value or a receive-only channel (e.g. passing a <-chan type field or a value obtained from another subscription). A subsequent check also panics on nil channels.

Common situations: Passing a channel stored in a struct as receive-only; forwarding subscription output by passing the receive side of a pipe; dynamically building Subscribe args and losing channel direction; passing a nil interface holding a typed nil channel.

Related errors


AI-assisted analysis of ethereum/go-ethereum@6bb0588ad8 (2026-08-15). Data as JSON: /api/errors/92ccc0d14a5d2d3b. Report an issue: GitHub.