denoland/deno · error · TypeError

ERR_ASYNC_CALLBACK

ERR_ASYNC_CALLBACK

Error message

hook.init must be a function

What it means

Thrown by Deno's node:async_hooks polyfill when async_hooks.createHook() receives an `init` field that is neither undefined nor a function. The AsyncHook constructor validates every callback slot (init, before, after, destroy, promiseResolve) before storing it, because the hook machinery later invokes these directly. `null` also triggers it, since the check is `!== undefined`, not falsiness.

Source

Thrown at ext/node/polyfills/internal/async_hooks.ts:429

  [after_symbol]: Fn;
  [destroy_symbol]: Fn;
  [promise_resolve_symbol]: Fn;

  constructor({
    init,
    before,
    after,
    destroy,
    promiseResolve,
  }: {
    init: Fn;
    before: Fn;
    after: Fn;
    destroy: Fn;
    promiseResolve: Fn;
  }) {
    if (init !== undefined && typeof init !== "function") {
      throw new ERR_ASYNC_CALLBACK("hook.init");
    }
    if (before !== undefined && typeof before !== "function") {
      throw new ERR_ASYNC_CALLBACK("hook.before");
    }
    if (after !== undefined && typeof after !== "function") {
      throw new ERR_ASYNC_CALLBACK("hook.after");
    }
    if (destroy !== undefined && typeof destroy !== "function") {
      throw new ERR_ASYNC_CALLBACK("hook.destroy");
    }
    if (promiseResolve !== undefined && typeof promiseResolve !== "function") {
      throw new ERR_ASYNC_CALLBACK("hook.promiseResolve");
    }

    this[init_symbol] = init;
    this[before_symbol] = before;
    this[after_symbol] = after;
    this[destroy_symbol] = destroy;

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Pass the function reference without parentheses: { init: onInit }
  2. If the callback is optional, omit the key entirely (or set undefined) — never null
  3. Guard the config before createHook: if (cfg.init !== undefined && typeof cfg.init !== 'function') throw new TypeError(...)
  4. If the config comes from JSON, rebind real functions from a module before creating the hook

Example fix

// before
async_hooks.createHook({ init: handleInit(), before, after, destroy });

// after
async_hooks.createHook({ init: handleInit, before, after, destroy });
Defensive patterns

Strategy: type-guard

Validate before calling

import async_hooks from 'node:async_hooks';
const hookConfig = { init, before, after, destroy, promiseResolve };
for (const [k, v] of Object.entries(hookConfig)) {
  if (v !== undefined && typeof v !== 'function') {
    throw new TypeError(`async_hooks.${k} must be a function, got ${typeof v}`);
  }
}
const hook = async_hooks.createHook(hookConfig);

Type guard

const isCallback = (v) => v === undefined || typeof v === 'function';
function isValidHookConfig(c) {
  return ['init', 'before', 'after', 'destroy', 'promiseResolve']
    .every((k) => isCallback(c[k]));
}

Try / catch

try {
  const hook = async_hooks.createHook(cfg);
} catch (err) {
  if (err?.code === 'ERR_ASYNC_CALLBACK') {
    // err.message names the offending field, e.g. 'hook.init must be a function'
    throw new Error(`Bad async hook config: ${err.message}`);
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling async_hooks.createHook({ init: ... }) where init is a string, number, object, null, or the RESULT of a function call instead of the function itself, e.g. createHook({ init: onInit() }) instead of createHook({ init: onInit }).

Common situations: Accidentally invoking the callback instead of passing a reference; building the hook config from JSON or a spread object where the function was lost or replaced; defaulting missing callbacks to null instead of omitting the key; DI containers or serializers stripping functions.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/126897893481a099. Report an issue: GitHub.