All topics
Patternsadvanced

Decorator Pattern in JavaScript

Wrapping a function or object to add extra behavior transparently, without modifying its original source code.

A decorator wraps a function or object to add behaviour, leaving the original unchanged.

The Decorator pattern is like a phone case: it wraps around the original phone, adding new capabilities (grip, protection, a kickstand) without opening up and modifying the phone's actual internal hardware — you can layer multiple cases/add-ons, and underneath it all, the original untouched phone is still doing exactly what it always did.

Key Concepts

1
    function withLogging(fn) {
      return function (...args) {
        console.log(`calling ${fn.name}`, args);
        const result = fn.apply(this, args);
        console.log(`returned`, result);
        return result;
      };
    }
calling ${fn.name}returned
2
    const loggedSave = withLogging(save);
3
They compose.
    const enhanced = withLogging(withRetry(withCache(fetchUser)));
4
Each wrapper adds one concern — logging, retrying, caching — and none of them knows about the others. fetchUser itself is unchanged and still testable on its own.
fetchUser
5
Which is the argument. Cross-cutting concerns live outside the function they apply to, rather than being copied into every function that needs them.
6
A useful one: timing.
    const timed = (fn) => async function (...args) {
      const t = performance.now();
      try {
        return await fn.apply(this, args);
      } finally {
        console.log(`${fn.name}: ${(performance.now() - t).toFixed(1)}ms`);
      }
    };
7
Three details that separate a correct decorator from a broken one.Preserve this. Use fn.apply(this, args), not fn(...args) — otherwise decorating a method loses its object.
thisfn.apply(this, args)fn(...args)
8
Handle async. A wrapper that does not await measures how long it took to start the work, not to finish it.
await
9
Preserve the signature. Name and arity are lost by default, which breaks anything inspecting them:
10
    Object.defineProperty(wrapper, 'name', { value: fn.name });
11
Class decorators are now standard syntax:
12
    class Service {
      @measure
      async fetchData() { }
    }
13
Where you meet them most is Angular (@Component, @Injectable) and NestJS.
@Component@Injectable
14
The costs. A stack trace runs through every wrapper, so a decorated function's errors point into decorator code. And each layer adds a call, which is irrelevant in most code and measurable in a hot path.
15
The judgement. Decorators suit concerns that genuinely apply to many functions — logging, caching, retries, authorisation. Wrapping one function to add one behaviour is just calling a function, with extra indirection.
16
What the interviewer is probing.1. "How do you decorate a function?" Probing: the technique. Stalls: "Edit it." Moves up: wrap it — return a new function that calls through and adds behaviour before or after, preserving the signature.
17
2. "What must a wrapper preserve?" Probing: the detail people miss. Stalls: "Just the return value." Moves up: this, all arguments, and the async nature — so use rest parameters, apply, and await where the inner is async.
18
3. "What are the ES decorators?" Probing: the language feature. Stalls: "The same thing." Moves up: syntax for applying this to classes and members declaratively, now standardised, and the basis of Angular and NestJS metadata.
19
4. "What is the debugging cost?" Probing: honesty. Stalls: "None." Moves up: stack traces fill with wrapper frames and the function name is lost unless you set it explicitly.