denoland/deno · error
Cannot register cleanup hook with same data twice
Error message
Cannot register cleanup hook with same data twice
What it means
Node-API's napi_add_cleanup_hook(env, fn, data) lets a native addon run code when the env tears down. Deno tracks registered (function, data) pairs and panics if the exact same pair is registered twice — stricter than some Node builds where duplicates are tolerated. It indicates the addon's registration logic executed more than once for the same hook and data pointer.
Source
Thrown at ext/napi/lib.rs:725
pub fn threadsafe_function_ref(&mut self) {
self.external_ops_tracker.ref_op();
}
pub fn threadsafe_function_unref(&mut self) {
self.external_ops_tracker.unref_op();
}
pub fn add_cleanup_hook(
&mut self,
hook: napi_cleanup_hook,
data: *mut c_void,
) {
let mut hooks = self.cleanup_hooks.borrow_mut();
if hooks
.iter()
.any(|pair| std::ptr::fn_addr_eq(pair.0, hook) && pair.1 == data)
{
panic!("Cannot register cleanup hook with same data twice");
}
hooks.push((hook, data));
}
pub fn remove_cleanup_hook(
&mut self,
hook: napi_cleanup_hook,
data: *mut c_void,
) {
let mut hooks = self.cleanup_hooks.borrow_mut();
match hooks
.iter()
.rposition(|&pair| std::ptr::fn_addr_eq(pair.0, hook) && pair.1 == data)
{
Some(index) => {
hooks.remove(index);
}
None => panic!("Cannot remove cleanup hook which was not registered"),View on GitHub (pinned to f7822238ca)
Solutions
- Guard registration with a static once-flag inside the addon's init
- Deduplicate how the addon is loaded — one specifier, one copy on disk
- Update the addon — many libraries already fixed double-registration for Deno
- If you own the addon, register the hook once per env and derive per-instance data internally
Example fix
// before — registers on every module init
napi_create_object(env, &exports);
napi_add_cleanup_hook(env, my_cleanup, data);
// after — register once per env
static bool g_registered = false;
if (!g_registered) {
napi_status s = napi_add_cleanup_hook(env, my_cleanup, data);
if (s == napi_ok) g_registered = true;
} Defensive patterns
Strategy: validation
Validate before calling
// C: register the cleanup hook at most once per env
static bool g_registered = false;
if (!g_registered) {
napi_status s = napi_add_cleanup_hook(env, my_cleanup, data);
if (s == napi_ok) g_registered = true;
} Prevention
- Guard napi_add_cleanup_hook with a once-flag
- Load each native addon through a single specifier and a single on-disk copy
- Re-test addons after Node-to-Deno migrations — duplicate-registration semantics are stricter here
When it happens
Trigger: A napi addon calling napi_add_cleanup_hook with an identical hook pointer and identical data pointer on every init, while the addon gets instantiated more than once in the same env — double require through two specifiers, duplicate copies on disk, worker-driven re-init.
Common situations: Native addons ported from Node where duplicate registration silently no-opped; bundlers/resolvers loading the same .node addon via two paths; version changes in the addon's init guard logic.
Related errors
- Cannot remove cleanup hook which was not registered
- Using cpu-features module is currently not supported
- Empty filepath.
- resolve hook must return { shortCircuit: true } or call next
- load hook must return { shortCircuit: true } or call nextLoa
AI-assisted analysis of denoland/deno@f7822238ca (2026-08-20).
Data as JSON: /api/errors/c05d6b85e7be5d2a.
Report an issue: GitHub.