typescript
41 lines · 9 steps
A method decorator that times async calls
A configurable TypeScript decorator wraps an async method to log how long it takes, but only past a threshold.
Explained by
highlit
1type AsyncMethod = (...args: any[]) => Promise<any>;
2
3function LogExecutionTime(thresholdMs = 0) {
4 return function <T extends AsyncMethod>(
5 target: object,
6 propertyKey: string | symbol,
7 descriptor: TypedPropertyDescriptor<T>,
8 ): TypedPropertyDescriptor<T> {
9 const original = descriptor.value;
10 if (!original) {
11 throw new Error(`@LogExecutionTime can only decorate methods`);
12 }
13
14 const label = `${target.constructor.name}.${String(propertyKey)}`;
15
16 descriptor.value = async function (this: unknown, ...args: unknown[]) {
17 const start = performance.now();
18 try {
19 return await original.apply(this, args);
20 } finally {
21 const elapsed = performance.now() - start;
22 if (elapsed >= thresholdMs) {
23 console.info(`[perf] ${label} took ${elapsed.toFixed(2)}ms`);
24 }
25 }
26 } as T;
27
28 return descriptor;
29 };
30}
31
32class ReportService {
33 @LogExecutionTime(50)
34 async generate(userId: string): Promise<Buffer> {
35 const rows = await this.repo.fetchRows(userId);
36 return this.renderer.toPdf(rows);
37 }
38
39 private repo!: { fetchRows(id: string): Promise<unknown[]> };
40 private renderer!: { toPdf(rows: unknown[]): Buffer };
41}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Decorator factories take arguments and return the real decorator, letting you configure behavior per usage.
- 2Wrapping the original method while preserving `this` via `apply` keeps the decorated method behaving identically.
- 3A `finally` block guarantees timing is recorded whether the wrapped call resolves or throws.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect app = FastAPI()
Building a WebSocket chat with FastAPI
websockets
broadcast
connection-management
Intermediate
9 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
php
<?php namespace App\Services;
How a password strength validator works in PHP
validation
regular-expressions
data-driven
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/a-method-decorator-that-times-async-calls-explained-typescript-fbaf/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.