ethereum/go-ethereum · error

can't create subscription after subscribe call has returned

Error message

can't create subscription after subscribe call has returned

What it means

Notifier.CreateSubscription panics when called after the subscribe RPC call has already returned (n.callReturned is set by takeSubscription when the server finishes handling the method). The subscription must be created synchronously inside the service method, before it returns, so the server can send the subscription ID back to the client.

Source

Thrown at rpc/subscription.go:124

	mu           sync.Mutex
	sub          *Subscription
	buffer       []any
	callReturned bool
	activated    bool
}

// CreateSubscription returns a new subscription that is coupled to the
// RPC connection. By default subscriptions are inactive and notifications
// are dropped until the subscription is marked as active. This is done
// by the RPC server after the subscription ID is send to the client.
func (n *Notifier) CreateSubscription() *Subscription {
	n.mu.Lock()
	defer n.mu.Unlock()

	if n.sub != nil {
		panic("can't create multiple subscriptions with Notifier")
	} else if n.callReturned {
		panic("can't create subscription after subscribe call has returned")
	}
	n.sub = &Subscription{ID: n.h.idgen(), namespace: n.namespace, err: make(chan error, 1)}
	return n.sub
}

// Notify sends a notification to the client with the given data as payload.
// If an error occurs the RPC connection is closed and the error is returned.
func (n *Notifier) Notify(id ID, data any) error {
	n.mu.Lock()
	defer n.mu.Unlock()

	if n.sub == nil {
		panic("can't Notify before subscription is created")
	} else if n.sub.ID != id {
		panic("Notify with wrong ID")
	}
	if n.activated {
		return n.send(n.sub, data)

View on GitHub (pinned to 6bb0588ad8)

Solutions

  1. Move CreateSubscription into the service method body, before it returns.
  2. If creation depends on async work, do the work first (or synchronously) and then create the subscription before returning.
  3. Do not store or reuse a Notifier across requests; request a fresh one via api.Notifier(...) in each call.

Example fix

// before
func (s *API) Subscribe(r *http.Request) (*rpc.Subscription, error) {
    notifier, _ := s.api.Notifier(r.Context())
    go func() { s.sub = notifier.CreateSubscription() }() // runs after return -> panic
    ...
}

// after
func (s *API) Subscribe(r *http.Request) (*rpc.Subscription, error) {
    notifier, _ := s.api.Notifier(r.Context())
    sub := notifier.CreateSubscription() // inside the call
    go func() { /* only notify here */ }()
    return sub, nil
}
Defensive patterns

Strategy: validation

Prevention

When it happens

Trigger: Capturing the *Notifier in a closure or storing it on the service struct, then calling CreateSubscription from a background goroutine after the subscribe method returned; calling CreateSubscription from inside notifier.Notify's send path or a later request.

Common situations: Starting a goroutine in the subscribe method that lazily creates the subscription; keeping the Notifier for a later handshake; refactoring a handler so the subscription creation moved outside the request scope.

Related errors


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