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
- Pass the function reference without parentheses: { init: onInit }
- If the callback is optional, omit the key entirely (or set undefined) — never null
- Guard the config before createHook: if (cfg.init !== undefined && typeof cfg.init !== 'function') throw new TypeError(...)
- 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
- Always pass function references, never call results
- Omit optional callbacks instead of setting null
- Never build hook configs from JSON/structuredClone — functions do not survive serialization
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
- Empty filepath.
- resolve hook must return { shortCircuit: true } or call next
- load hook must return { shortCircuit: true } or call nextLoa
- Unknown built-in module
- Module already loaded
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/126897893481a099.
Report an issue: GitHub.