grafana/k6 · error

's callback isn't a callable function

Error message

%s's callback isn't a callable function

What it means

Fires during timer initialization when the first argument passed to setTimeout or setInterval exists but is not a callable function value (e.g. a string, number, or object). This is a generic guard at the top of timerInitialization: the callback cannot be invoked as a sobek.Callable (nil or non-callable), so k6 aborts setup of the timer before registering it with the event loop, since there is nothing to invoke when the timer fires.

Solutions

  1. Pass an actual function: setTimeout(() => {...}, 100).
  2. Check for typos in the callback identifier so it doesn't resolve to undefined.
  3. Guard the call site: only schedule the timer when typeof cb === 'function'.

Example fix

// before
const cb = maybeHandler; // undefined
setTimeout(cb, 1000);
// after
if (typeof maybeHandler === 'function') {
  setTimeout(maybeHandler, 1000);
}
Defensive patterns

Strategy: type-guard

Validate before calling

if (typeof cb !== 'function') {
  throw new TypeError('setTimeout requires a function callback');
}
setTimeout(cb, delay);

Type guard

function isCallable(v) {
  return typeof v === 'function';
}

Prevention

When it happens

Trigger: Calling setTimeout(undefined, 100), setTimeout(null), setTimeout() with no arguments, or passing a non-function (string, object) where a function is expected; also runTask with a nil callback.

Common situations: Conditional code that builds the callback but leaves it undefined; typo'd function names (setTimeout(myFuncton, 100) -> undefined variable becomes undefined under non-strict handling); refactoring that removed the handler.

Understand the failure class

Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.

Related errors


AI-assisted analysis of grafana/k6@3fcf5388d7 (2026-09-20). Data as JSON: /api/errors/60361083730ba5e1. Report an issue: GitHub.

Appendix: source

Thrown at internal/js/tc55/timers/timers.go:142

// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timer-initialisation-steps
// NOTE: previousId from the specification is always send and it is basically id
//
// Repeated timers must reuse the registration-time metric context when they rearm.
func (e *timers) timerInitialization(
	callback sobek.Callable, timeout float64, args []sobek.Value, repeat bool, id uint64,
) {
	// skip all the nesting stuff as we do not care about them
	if timeout < 0 {
		timeout = 0
	}

	name := setTimeoutName
	if repeat {
		name = setIntervalName
	}

	if callback == nil {
		common.Throw(e.vu.Runtime(), fmt.Errorf("%s's callback isn't a callable function", name))
	}

	t := &timer{
		id:          id,
		nextTrigger: time.Now().Add(time.Duration(timeout * float64(time.Millisecond))),
		name:        name,
	}
	t.task = func() error {
		return e.runTask(t, callback, timeout, args, repeat)
	}
	e.runAfterTimeout(t)
}

// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#run-steps-after-a-timeout
// Notes: this just takes timers as makes the implementation way easier and we do not currently need
// most of the functionality provided
func (e *timers) runAfterTimeout(t *timer) {
	e.timers[t.id] = t

View on GitHub (pinned to 3fcf5388d7)