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.
High because async handlers whose Promises are not awaited silently swallow errors, making plugin failures invisible and corrupting write pipelines without any logged indication.
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
}
}
ID: plugin-extension-architecture.hook-system.async-hooks
Severity: high
What to look for: Check whether hook handlers can be async functions (returning Promises). Examine:
await handler results or call .then() on them?Promise<void> or async return types?tapAsync and tapPromise variants available alongside tap?
Real-world async hook examples:async (request, reply) => {}tapAsync(name, callback) and tapPromise(name, handler) alongside sync tapexpress-async-errors — this is a common failure pointPass 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
}
}