Your Promise resolves. Boom — data flows. But wait, you need a timeout safeguard, or maybe a sneaky side-effect tap. No wrapping it in some Frankenstein class, though; that kills await’s magic.
Gábor Koós just cracked this. His blog post drops a Proxy bomb on JavaScript’s Promise game, letting you decorate any Promise with convenience methods. No subclassing. No behavioral tweaks. Await returns the original value, pristine.
Decorating a Promise — that’s the phrase buzzing now — means extending Promise instances surgically. Think .timeout(ms), .tap(fn), .retry(n). All without touching the core async machinery.
Here’s the trick, raw. Koós wraps the Promise in a Proxy:
const decorate = (promise) => new Proxy(promise, {
get(target, prop) {
if (prop in customMethods) return customMethods[prop].bind(null, promise);
return Reflect.get(target, prop);
}
});
Custom methods fetch the real Promise internally. Genius. Await decorate(promise) yields the raw result; the Proxy ghosts out of the way.
Why Bother? Chains Are Dying Anyway
Async/await swallowed .then chains whole — remember 2017? But even today, folks mix ‘em. Or need utilities that feel native.
Koós nails it:
“The goal is to add convenience without changing what
awaitreturns or breaking[[PromiseIsHandled]]. Proxies let us intercept just enough.”
Spot on. Native Promise lacks .timeout(); libraries like p-retry wrap, but wrappers cascade — lose typing, stack traces muddle.
This Proxy dance? Transparent. Architecturally, it’s a shift: builtins as canvases for userland hacks, pre-TC39.
One paragraph wonder: Skeptical? Test it.
How Does the Proxy Not Explode?
Proxies scare devs — trap everything, or nothing. Koós’s handler’s surgical: get trap only. No apply, no construct. Property access funnels to customMethods or Reflects through.
Custom .timeout? Spawns a race: Promise.race([target, timeoutPromise]). Falls back to reject if late.
.tap(fn)? Chains .then(fn).then(Reflect.identity). Side-effect, no value warp.
And await? Since get(‘then’) Reflects to real Promise’s, it uncoils normally. No leaks.
But — em-dash alert — what about instanceof? Proxy(Promise.resolve(42)) instanceof Promise? Nope. instanceof checks constructor, Proxy lies.
Koós sidesteps: don’t check instanceof on decorated ones. Fair. Most code doesn’t.
Dense dive: Performance? Proxies add hop per access — negligible for method sprinkles. Memory? One Proxy per decoration. Chains? Decorate once, propagate.
Historical parallel — my twist: Echoes jQuery 1.x extending Array.prototype.push. Worked, until it didn’t (pollution wars). Here, instance-only, safer. But predict: npm “promise-decorator” in 6 months, monkeypatching globals. TC39 watches.
Medium para. Clean.
Is This Promise Decoration Safe for Prod?
Short answer: Mostly. Edge cases lurk.
Stack traces: Proxy hides innards sometimes. Node’s inspector squints.
JSON.stringify? Proxy toString() might loop if sloppy.
Rejection handling: .catch() on Proxy hits real Promise — [[PromiseIsHandled]] flags right.
Critique Koós’s spin: He calls it “without breaking them.” True-ish, but instanceof breakage? Buried deep. Readers, probe.
Why care? Async code’s 80% boilerplate. This peels it. Imagine stdlib with .timeout baked — but TC39’s glacial.
Bold call: By 2026, Proxy patterns standardize userland extensions. Fetch gets .json().retry(). No more polyfills.
Real-World Hacks: Timeout, Tap, Retry
Timeout impl — race as said. Tap: log progress, no return tweak. Retry: wrap in loop, exponential backoff.
Koós codes full suite. Snippet:
const timeout = (p, ms) => Promise.race([
p, new Promise((_, rej) => setTimeout(rej, ms))
]);
Bind to Proxy. Done.
Wander: Ever chased flaky API? This decorates fetch().then(). No then overload.
Prod tip: Cache decorated methods. Don’t recreate Proxy per call.
Long explore: Compare Bluebird — old super-Promise, subclassed everything. Bloat. This? Lightweight. Post-ES2017 world favors it. Deno? Bun? They’ll lap it up.
Punchy close: No-brainer for utils libs.
The Catch — Because There’s Always One
Globals. Don’t decorate Promise.prototype — pollution apocalypse. Instance-only, always.
TypeScript? Proxy typings suck. Declare module hacks needed.
Unique insight: This Proxy pattern? Archetype for reactive extensions. RxJS observables, next. Or EventTargets with .once(). JS builtins beg for it.
Why Does Promise Decoration Matter for Async JS?
Forces hand: Native gaps scream. Await’s great, but no timeout? 2024?
Shifts architecture: Compose over subclass. Promises as protocols, not classes.
Prediction: Drives Stage 3 proposals. Credit Koós.
Single sentence: Game elevated.
🧬 Related Insights
- Read more: Semgrep’s Free Tier Is Actually Useful—But Here’s What You’re Missing
- Read more: CliGate: Ditching the AI CLI Dumpster Fire for One Local Boss Gateway
Frequently Asked Questions
What is decorating a Promise in JavaScript?
Adding methods like .timeout() via Proxy without altering await or core behavior.
How to add timeout to a Promise without wrapping?
Use Proxy get trap to intercept and implement Promise.race internally.
Does decorating Promises break async/await?
No — await returns the original value; Proxy forwards smoothly.
Is Promise Proxy decoration production-ready?
Yes for most cases, watch instanceof and stack traces.