Skip to content

Timers return numbers: setTimeout(...).unref() / .ref() / .hasRef() / .refresh() throw TypeError #54

Closed

Description

@hassidan

Test I ran

node timer_handles.ts
slice run timer_handles.ts
timer_handles.ts
const show = async (k: string, f: () => unknown) => { let v; try { v = await Promise.race([Promise.resolve().then(f), new Promise((r) => setTimeout(() => r("NEVER SETTLED (2000 ms)"), 2000))]); } catch (e: any) { v = "THROWS " + e.name + ": " + String(e.message).slice(0, 100); } console.log(k.padEnd(52) + JSON.stringify(v)); };
(async () => {
  const h: any = setTimeout(() => {}, 10);
  await show("typeof setTimeout(...)", () => typeof h);
  await show("typeof .unref/.ref/.hasRef/.refresh", () => [typeof h.unref, typeof h.ref, typeof h.hasRef, typeof h.refresh]);
  await show("setTimeout(() => {}, 10).unref()", () => { h.unref(); return "ok"; });
  await show("setInterval(() => {}, 1000).unref()", () => { const i: any = setInterval(() => {}, 1000); try { i.unref(); return "ok"; } finally { clearInterval(i); } });
  clearTimeout(h);
})();

Result

Node v24.22.2:

typeof setTimeout(...)                              "object"
typeof .unref/.ref/.hasRef/.refresh                 ["function","function","function","function"]
setTimeout(() => {}, 10).unref()                    "ok"
setInterval(() => {}, 1000).unref()                 "ok"

Slice v0.10.7:

typeof setTimeout(...)                              "number"
typeof .unref/.ref/.hasRef/.refresh                 ["undefined","undefined","undefined","undefined"]
setTimeout(() => {}, 10).unref()                    "THROWS TypeError: not a callable function"
setInterval(() => {}, 1000).unref()                 "THROWS TypeError: not a callable function"

What is wrong

setTimeout / setInterval return a number, so the Node Timeout methods do not exist and calling them throws TypeError: not a callable function.

A numeric id is what browsers and WinterTC specify, so this is a Node-compatibility gap, not a spec violation. It matters because setTimeout(fn, ms).unref() and setInterval(fn, ms).unref() are common in npm packages (keep-alive pings, cache sweepers, connection pools, metrics flushers), and there the call is unconditional, so the package throws on load or on first use.

A small wrapper object with no-op ref() / unref() (returning this), hasRef(), refresh() and [Symbol.toPrimitive]() returning the id would keep clearTimeout(handle) working and let those packages run. node:timers is listed as supported in COMPATIBILITY.md; the handle methods are not mentioned either way.

Environment

  • Slice v0.10.7, commit ea7c70c, built with cargo +1.96.0 build --release --locked --bin slice
  • Reference: Node.js v24.22.2 (same script, same machine)
  • Ubuntu 26.04, Linux 7.0, x86_64
  • The same output is produced by the v0.10.5 tag, so this is not a regression in 0.10.6/0.10.7.

Activity

  1. siktec-lab commented on Nov 11, 2026

    @siktec-lab
    Collaborator

    Addressed in #67 (�0.10.11): updated setTimeout, setInterval, and setImmediate to return Node-compatible Timeout / Immediate handle objects ( ypeof h === 'object') with .unref(), .ref(), .hasRef(), .refresh(), Symbol.toPrimitive, and �alueOf(), and updated the event loop so unref'd timers do not keep the isolate alive when no ref'd work remains.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions