Skip to content

Instantly share code, notes, and snippets.

@pi0
Last active August 27, 2025 01:57
Show Gist options
  • Save pi0/42f1c168d24d5bdef336f7a0f5bf2026 to your computer and use it in GitHub Desktop.
Save pi0/42f1c168d24d5bdef336f7a0f5bf2026 to your computer and use it in GitHub Desktop.
Thenable vs Promise instance check
clk: ~4.24 GHz
cpu: Apple M4 Pro
runtime: node 22.18.0 (arm64-darwin)
benchmark avg (min … max) p75 / p99 (min … top 1%)
------------------------------------------------- -------------------------------
• single check per iteration
------------------------------------------------- -------------------------------
instanceof Promise 1.55 ns/iter 1.59 ns 2.07 ns ▃█▂▇▄▁▂▁▁▁▁
typeof x.then === "function" 3.72 ns/iter 3.92 ns 4.87 ns ▁▂▄█▃▆▃▁▁▁▁
summary
instanceof Promise
2.4x faster than typeof x.then === "function"
• batch of 16 checks per iteration
------------------------------------------------- -------------------------------
instanceof Promise (x16) 26.84 ns/iter 28.02 ns 30.80 ns ▂▃█▅▅▆█▃▂▂▂
typeof x.then === "function" (x16) 63.57 ns/iter 65.14 ns 68.84 ns ▁▃▄▇███▆▄▂▁
summary
instanceof Promise (x16)
2.37x faster than typeof x.then === "function" (x16)
// bench.mjs
import { bench, summary, compact, group, run } from "mitata";
// Two checks
const isPromiseInstance = (x) => x instanceof Promise;
const isThenable = (x) => typeof x?.then === "function";
// Mixed inputs: native Promises, thenables, and non-promises
const items = [
Promise.resolve(1),
Promise.reject(new Error("x")).catch(() => 0), // settled promise
{ then: (f) => f?.() }, // plain thenable
42,
null,
{ a: 1 },
async () => 1, // function, not thenable itself
"str",
];
// Simple rotator to avoid constant-folding
let i = 0;
const next = () => {
const v = items[i];
i = (i + 1) % items.length;
return v;
};
// Sink to prevent dead-code elimination
let sink = 0;
group("single check per iteration", () => {
summary(() => {
compact(() => {
bench("instanceof Promise", () => {
const v = next();
sink ^= isPromiseInstance(v) | 0;
});
bench('typeof x.then === "function"', () => {
const v = next();
sink ^= isThenable(v) | 0;
});
});
});
});
group("batch of 16 checks per iteration", () => {
summary(() => {
compact(() => {
bench("instanceof Promise (x16)", () => {
let acc = 0;
for (let k = 0; k < 16; k++) acc ^= isPromiseInstance(next()) | 0;
sink ^= acc;
});
bench('typeof x.then === "function" (x16)', () => {
let acc = 0;
for (let k = 0; k < 16; k++) acc ^= isThenable(next()) | 0;
sink ^= acc;
});
});
});
});
await run();
// tiny use of sink so Node keeps the code paths alive
if (sink === 123_456_789) console.log("ignore:", sink);
@aquapi
Copy link

aquapi commented Aug 27, 2025

You can use this feature to avoid next() call overhead:

bench('instanceof Promise', function*() {
  yield {
    [0]: next,
    bench: (val) => {
      sink ^= val;
      // or with import('mitata').do_not_optimize
      do_not_optimize(val);
    }
  }
});

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