Async hooks supported
Why it matters
Most real-world plugins need to do async work — call external APIs, query databases, read files. If the hook invocation loop does not await handler return values, any plugin that returns a Promise appears to work but silently drops its errors. The host continues as though the handler succeeded while the underlying operation failed. This creates data integrity issues in write operations, invisible failures in webhook processors, and phantom results in request pipelines. ISO 25010 reliability.fault-tolerance requires that errors propagate to where they can be handled, not vanish into unhandled rejections.
Severity rationale
High because async handlers whose Promises are not awaited silently swallow errors, making plugin failures invisible and corrupting write pipelines without any logged indication.
Remediation
Audit every hook invocation site and ensure the loop awaits each handler. For sequential semantics, use for...of with await; for parallel semantics, use Promise.all. Unhandled rejections must be caught and attributed to the specific plugin.
async function invokeHook(name: string, context: HookContext): Promise<void> {
const handlers = this.registry.get(name) ?? [];
for (const handler of handlers) {
await handler(context); // awaits each handler in sequence
}
}
Detection
-
ID:
async-hooks -
Severity:
high -
What to look for: Check whether hook handlers can be async functions (returning Promises). Examine:
- Does the hook invocation code
awaithandler results or call.then()on them? - Are handlers explicitly typed to allow
Promise<void>orasyncreturn types? - What happens if a handler returns a Promise but the host doesn't await it? (Silent failure pattern)
- For tapable-based systems: are
tapAsyncandtapPromisevariants available alongsidetap? Real-world async hook examples:
- Fastify: all hooks support async handlers natively —
async (request, reply) => {} - webpack tapable:
tapAsync(name, callback)andtapPromise(name, handler)alongside synctap - Express: async middleware requires wrapping or
express-async-errors— this is a common failure point
- Does the hook invocation code
-
Pass criteria: Count all async-capable hooks. Hook handlers can return Promises and the host correctly awaits them. If the system distinguishes sync and async hooks, both variants are available. Errors from async handlers are caught and propagated (not silently swallowed). At least 100% of hooks that invoke async operations must support async execution.
-
Fail criteria: Hook handlers that return Promises are not awaited — the host fires and forgets. OR async handlers are not supported and the documentation doesn't mention this limitation. OR errors from async handlers are silently swallowed (the Promise rejection is unhandled).
-
Skip (N/A) when: The hook system is intentionally synchronous-only AND this is documented AND the use case doesn't require async operations (e.g., synchronous transform pipelines like Babel visitors).
-
Detail on fail:
"Hook handlers return values are ignored — the invocation loop calls handler() but does not await the result. Async handlers silently fail because their Promise rejections are never caught. Any plugin that needs to do async work (API calls, file I/O, database queries) will appear to work but silently drop errors." -
Remediation: Most real-world plugins need to perform async work. If the hook system doesn't support async handlers, plugin authors will resort to workarounds (synchronous blocking, fire-and-forget) that cause subtle bugs.
// Invoking hooks with async support: async function invokeHook(name: string, context: HookContext): Promise<void> { const handlers = this.registry.get(name) ?? []; for (const handler of handlers) { await handler(context); // Sequential: await each handler } }
External references
- iso-25010:2011 · reliability.fault-tolerance — Fault tolerance — async errors from plugin handlers must be caught and not crash the host
- iso-25010:2011 · maintainability.modularity
Taxons
History
- 2026-04-18·v1.0.0·Initial import from plugin-extension-architecture·automated