grafana/k6 · error
handler for %q event isn't a callable function
Error message
handler for %q event isn't a callable function
What it means
Thrown by k6's gRPC stream API when stream.on(event, handler) is called with a handler that is not callable. Sobek converts the JS argument to the Go func(sobek.Value, sobek.Value) type; a non-function value (undefined, null, number, object) converts to nil, and on() (internal/js/modules/k6/grpc/stream.go:306-309) throws via common.Throw. Valid event types registered through eventListeners are 'data', 'error', and 'end'.
Source
Thrown at internal/js/modules/k6/grpc/stream.go:308
}
}
}
func (s *stream) processSendError(err error) {
if errors.Is(err, io.EOF) {
s.logger.WithError(err).Debug("skip sending a message stream is cancelled/finished")
err = nil
}
s.tq.Queue(func() error {
return s.closeWithError(err)
})
}
// on registers a handler for a certain event type
func (s *stream) on(event string, handler func(sobek.Value, sobek.Value) (sobek.Value, error)) {
if handler == nil {
common.Throw(s.vu.Runtime(), fmt.Errorf("handler for %q event isn't a callable function", event))
}
if err := s.eventListeners.add(event, handler); err != nil {
s.vu.State().Logger.Warnf("can't register %s event handler: %s", event, err)
}
}
// write writes a message to the stream
func (s *stream) write(input sobek.Value) {
if s.writingState != opened {
return
}
if common.IsNullish(input) {
s.logger.Warnf("can't send empty message")
return
}
View on GitHub (pinned to 93accf6570)
Solutions
- Check the event name from the message and inspect the second argument at that call site — it must be a function reference.
- Verify the handler is actually exported/defined: add console.log(typeof handler) before stream.on.
- Pass the function reference (onData), not its invocation result (onData()).
Example fix
// before
import { onMessage } from './handlers.js'; // not exported -> undefined
stream.on('data', onMessage);
// after
import { onMessage } from './handlers.js'; // export function onMessage(msg) {...}
stream.on('data', onMessage); Defensive patterns
Strategy: type-guard
Validate before calling
const STREAM_EVENTS = new Set(['data', 'error', 'end']);
function safeOn(stream, event, handler) {
if (!STREAM_EVENTS.has(event)) throw new Error(`unknown stream event '${event}'`);
if (typeof handler !== 'function') throw new Error(`handler for '${event}' must be a function, got ${typeof handler}`);
stream.on(event, handler);
} Type guard
const isCallable = (v) => typeof v === 'function';
Prevention
- Assert typeof handler === 'function' during development with a tiny wrapper.
- Import handlers from modules you own and re-export explicitly; avoid deep dynamic imports.
- Lint for stream.on calls whose second argument is a literal or identifier that is never defined.
When it happens
Trigger: stream.on('data', undefined) — typically a handler function whose import/definition is missing or misspelled; stream.on('end', { handle: fn }) (object instead of function); passing the result of calling the function fn() instead of the reference fn.
Common situations: Refactoring that renames or removes the handler but leaves the stream.on call; conditional imports (import { onData } from './handlers.js' where onData is not exported); copy-paste from docs where the handler placeholder was never replaced.
Related errors
- method to invoke cannot be empty
- request cannot be nil
- empty gRPC client
- no gRPC connection, you must call connect first
- must be an object with key-value pairs
AI-assisted analysis of grafana/k6@93accf6570 (2026-08-15).
Data as JSON: /api/errors/3c3bf9b2139e2919.
Report an issue: GitHub.