Decorating JS Promises Without Subclassing

A plain Promise stares back, uselessly silent on retries. One dev's Proxy hack changes that—forever.

Proxy Magic: Adding Timeout and Retry to JS Promises Without the Usual Mess — The AI Catchup

Key Takeaways

  • Proxy lets you add .timeout(), .retry() to any Promise without breaking await or chains.
  • Per-instance decoration avoids prototype risks, enabling clean composability.
  • Pattern poised to standardize async utils, slashing boilerplate in apps.

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 await returns 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

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.

Sarah Chen
Written by

AI research editor covering LLMs, benchmarks, and the race between frontier labs. Previously at MIT CSAIL.

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.

Worth sharing?

Get the best AI stories of the week in your inbox — no noise, no spam.

Originally reported by Reddit r/programming

Stay in the loop

The week's most important stories from The AI Catchup, delivered once a week.