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 { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, DeepPartial } from 'typeorm'; import { User } from './entities/user.entity';
Building a CRUD service in NestJS
crud
dependency-injection
repository-pattern
Intermediate
7 steps
rust
use std::sync::Arc; use std::time::Duration; use axum::{
Long-polling with Axum and Tokio Notify
long-polling
concurrency
shared-state
Advanced
8 steps
php
<?php namespace App\Queue;
A stable priority job queue in PHP
priority-queue
data-structures
closures
Intermediate
8 steps
typescript
import { Controller, Get, Param, Query, Redirect, NotFoundException } from '@nestjs/common'; import { LinksService } from './links.service'; @Controller('links')
Handling HTTP redirects in NestJS
redirects
routing
decorators
Intermediate
7 steps
javascript
class InfiniteScroll { constructor(sentinel, { loadMore, root = null, rootMargin = '200px' } = {}) { this.sentinel = sentinel; this.loadMore = loadMore;
Infinite scroll with IntersectionObserver
intersectionobserver
pagination
async
Intermediate
10 steps
typescript
import { CanActivate, ExecutionContext, Injectable,
How guards chain auth in NestJS
guards
authentication
authorization
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.