JavaScript Decorator Pattern: Complete Guide
A complete guide to the JavaScript decorator pattern. Covers function wrapping, method decoration, class decorators, decorator composition, memoization decorators, retry decorators, and the TC39 decorators proposal for modern JavaScript.
The decorator pattern wraps existing functionality with additional behavior without modifying the original code. In JavaScript, decorators leverage higher-order functions and closures to transparently enhance objects, methods, and classes at runtime.
For practical logging decorators in production architectures, see Using Decorators for Logging in JS Architecture.
Function Decorators
function withLogging(fn, label) {
return function (...args) {
console.log(`[${label}] Called with:`, args);
const start = performance.now();
try {
const result = fn.apply(this, args);
if (result instanceof Promise) {
return result.then((val) => {
console.log(`[${label}] Resolved in ${(performance.now() - start).toFixed(2)}ms:`, val);
return val;
}).catch((err) => {
console.error(`[${label}] Rejected in ${(performance.now() - start).toFixed(2)}ms:`, err);
throw err;
});
}
console.log(`[${label}] Returned in ${(performance.now() - start).toFixed(2)}ms:`, result);
return result;
} catch (error) {
console.error(`[${label}] Threw in ${(performance.now() - start).toFixed(2)}ms:`, error);
throw error;
}
};
}
function withRetry(fn, maxRetries = 3, delayMs = 1000) {
return async function (...args) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await fn.apply(this, args);
} catch (error) {
lastError = error;
console.warn(`Attempt ${attempt}/${maxRetries} failed:`, error.message);
if (attempt < maxRetries) {
const backoff = delayMs * Math.pow(2, attempt - 1);
await new Promise((r) => setTimeout(r, backoff));
}
}
}
throw lastError;
};
}
function withMemoize(fn, keyFn = (...args) => JSON.stringify(args)) {
const cache = new Map();
const memoized = function (...args) {
const key = keyFn(...args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
memoized.clearCache = () => cache.clear();
memoized.cacheSize = () => cache.size;
return memoized;
}
// Usage
function fetchUser(id) {
return fetch(`/api/users/${id}`).then((r) => r.json());
}
const decoratedFetch = withLogging(withRetry(fetchUser, 3, 500), "fetchUser");Method Decorators
function decorateMethod(obj, methodName, decorator) {
const original = obj[methodName];
if (typeof original !== "function") {
throw new Error(`${methodName} is not a method`);
}
obj[methodName] = decorator(original, methodName);
return obj;
}
function readonly(obj, methodName) {
const original = obj[methodName];
Object.defineProperty(obj, methodName, {
value: original,
writable: false,
configurable: false,
});
return obj;
}
function deprecate(message) {
return function (fn, name) {
let warned = false;
return function (...args) {
if (!warned) {
console.warn(`DEPRECATED: ${name}() - ${message}`);
warned = true;
}
return fn.apply(this, args);
};
};
}
function throttle(limitMs) {
return function (fn) {
let lastCall = 0;
let lastResult;
return function (...args) {
const now = Date.now();
if (now - lastCall >= limitMs) {
lastCall = now;
lastResult = fn.apply(this, args);
}
return lastResult;
};
};
}
function debounce(delayMs) {
return function (fn) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delayMs);
};
};
}
// Usage
const api = {
search(query) {
console.log(`Searching: ${query}`);
return [`Result for: ${query}`];
},
legacyMethod() {
return "old result";
},
};
decorateMethod(api, "search", throttle(1000));
decorateMethod(api, "legacyMethod", deprecate("Use newMethod() instead"));
api.search("javascript"); // Executes
api.search("patterns"); // Throttled, returns last result
api.legacyMethod(); // Logs deprecation warning onceClass Decorator Pattern
function withTimestamps(BaseClass) {
return class extends BaseClass {
constructor(...args) {
super(...args);
this.createdAt = Date.now();
this.updatedAt = Date.now();
}
touch() {
this.updatedAt = Date.now();
return this;
}
};
}
function withSerialization(BaseClass) {
return class extends BaseClass {
toJSON() {
const data = {};
for (const key of Object.keys(this)) {
if (typeof this[key] !== "function") {
data[key] = this[key];
}
}
return data;
}
static fromJSON(json) {
const data = typeof json === "string" ? JSON.parse(json) : json;
const instance = new this();
Object.assign(instance, data);
return instance;
}
};
}
function withValidation(schema) {
return function (BaseClass) {
return class extends BaseClass {
validate() {
const errors = {};
for (const [field, rules] of Object.entries(schema)) {
if (rules.required && !this[field]) {
errors[field] = `${field} is required`;
}
if (rules.type && typeof this[field] !== rules.type) {
errors[field] = `${field} must be ${rules.type}`;
}
if (rules.min !== undefined && this[field] < rules.min) {
errors[field] = `${field} must be >= ${rules.min}`;
}
}
return {
valid: Object.keys(errors).length === 0,
errors,
};
}
};
};
}
// Compose class decorators
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
greet() {
return `Hello, ${this.name}`;
}
}
const EnhancedUser = withSerialization(
withTimestamps(
withValidation({
name: { required: true, type: "string" },
email: { required: true, type: "string" },
})(User)
)
);
const user = new EnhancedUser("Alice", "alice@test.com");
console.log(user.createdAt); // timestamp
console.log(user.validate()); // { valid: true, errors: {} }
console.log(user.toJSON()); // { name, email, createdAt, updatedAt }Decorator Composition Pipeline
function compose(...decorators) {
return function (target) {
return decorators.reduceRight((decorated, decorator) => decorator(decorated), target);
};
}
function pipe(...decorators) {
return function (target) {
return decorators.reduce((decorated, decorator) => decorator(decorated), target);
};
}
// For functions
function withTiming(fn) {
return function (...args) {
const start = performance.now();
const result = fn.apply(this, args);
const duration = performance.now() - start;
console.log(`${fn.name || "anonymous"}: ${duration.toFixed(2)}ms`);
return result;
};
}
function withValidation(fn) {
return function (...args) {
for (const arg of args) {
if (arg === null || arg === undefined) {
throw new Error("Arguments cannot be null or undefined");
}
}
return fn.apply(this, args);
};
}
function withErrorBoundary(fn) {
return function (...args) {
try {
return fn.apply(this, args);
} catch (error) {
console.error(`Error in ${fn.name}:`, error.message);
return null;
}
};
}
// Compose: right-to-left (validation runs first, then timing, then error boundary)
const enhancedProcess = compose(
withErrorBoundary,
withTiming,
withValidation
)(function processData(data) {
return data.map((item) => item.toUpperCase());
});
// Pipe: left-to-right (same order, more readable)
const pipedProcess = pipe(
withValidation,
withTiming,
withErrorBoundary
)(function processData(data) {
return data.map((item) => item.toUpperCase());
});
console.log(enhancedProcess(["hello", "world"])); // ["HELLO", "WORLD"]
console.log(enhancedProcess(null)); // null (caught by boundary)Rate Limiter Decorator
function withRateLimit(fn, { maxCalls, windowMs, onLimitReached }) {
const calls = [];
return function (...args) {
const now = Date.now();
// Remove expired entries
while (calls.length > 0 && now - calls[0] > windowMs) {
calls.shift();
}
if (calls.length >= maxCalls) {
if (onLimitReached) {
onLimitReached(calls.length, maxCalls);
}
throw new Error(
`Rate limit exceeded: ${maxCalls} calls per ${windowMs}ms`
);
}
calls.push(now);
return fn.apply(this, args);
};
}
function withCircuitBreaker(fn, { threshold = 5, resetTimeout = 30000 } = {}) {
let failures = 0;
let state = "CLOSED"; // CLOSED, OPEN, HALF_OPEN
let lastFailureTime = null;
return async function (...args) {
if (state === "OPEN") {
if (Date.now() - lastFailureTime > resetTimeout) {
state = "HALF_OPEN";
} else {
throw new Error("Circuit breaker is OPEN");
}
}
try {
const result = await fn.apply(this, args);
if (state === "HALF_OPEN") {
state = "CLOSED";
failures = 0;
}
return result;
} catch (error) {
failures++;
lastFailureTime = Date.now();
if (failures >= threshold) {
state = "OPEN";
console.warn(`Circuit breaker OPENED after ${failures} failures`);
}
throw error;
}
};
}
// Usage
const apiCall = withRateLimit(
withCircuitBreaker(
async (endpoint) => {
const res = await fetch(endpoint);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
},
{ threshold: 3, resetTimeout: 10000 }
),
{ maxCalls: 10, windowMs: 60000, onLimitReached: (c, m) => console.warn(`Rate: ${c}/${m}`) }
);| Decorator Type | Wraps | Purpose | Example |
|---|---|---|---|
| Function decorator | Functions | Add cross-cutting concerns | logging, memoize, retry |
| Method decorator | Object methods | Enhance class behavior | throttle, deprecate |
| Class decorator | Classes | Extend with new capabilities | timestamps, serialization |
| Composition | Multiple decorators | Chain enhancements | pipe, compose |
| Resilience | Async functions | Fault tolerance | circuit breaker, rate limit |
Rune AI
Key Insights
- Function decorators wrap existing functions with additional behavior: Higher-order functions add logging, timing, retry, and caching transparently without modifying the original code
- Method decorators enhance individual object methods selectively: Apply throttle, debounce, or deprecation warnings to specific methods without affecting the entire class
- Class decorators extend classes with new capabilities at the class level: Wrapping classes adds timestamps, serialization, and validation through composition rather than inheritance
- Compose and pipe utilities chain multiple decorators predictably: Right-to-left (compose) or left-to-right (pipe) composition creates readable decorator pipelines
- Resilience decorators like circuit breaker and rate limiter protect production services: These patterns prevent cascade failures and enforce usage limits at the function boundary
Frequently Asked Questions
How is the decorator pattern different from inheritance?
Will TC39 decorators replace this pattern?
How do I preserve the original function's name and length?
Can decorators cause performance issues?
How do I unit test decorated functions?
Conclusion
The decorator pattern adds behavior to functions, methods, and classes without modifying their source code. Function decorators wrap with logging, memoization, retry, and rate limiting. Class decorators add timestamps, serialization, and validation. Composition utilities like pipe and compose chain decorators cleanly. For production logging architectures using decorators, see Using Decorators for Logging in JS Architecture. For the strategy pattern that complements decorator-based approach selection, review JavaScript Strategy Pattern: Complete Guide.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.