Appearance
Timer Patterns
Promise-based timers with explicit state management and cancellation, plus lightweight utilities for measuring elapsed time.
When to Use
- Scheduling a one-shot delayed action that can be canceled
- Measuring elapsed time for performance logging
- Tracking lap times across multiple checkpoints
- Needing to
awaita timer as a promise
The Patterns
Timer class
A promise-based timer with four states: READY, RUNNING, COMPLETED, CANCELED. The timer is thenable — it works with await.
javascript
const timer = Timer.start({
duration: 5000,
name: "session-timeout",
handler: () => logout(),
});
// Cancel if user interacts
document.addEventListener("click", () => {
timer.cancel();
});
// Or await it
try {
await timer;
console.log("Timer completed");
} catch {
console.log("Timer was canceled");
}Implementing Timer
Wraps setTimeout in a promise-based state machine with explicit READY, RUNNING, COMPLETED, and CANCELED transitions:
javascript
class Timer {
#state = "ready";
#name;
#duration;
#resolvers = Promise.withResolvers();
#timeoutId;
static start({ duration, name = "", handler }) {
return new Timer({
duration, name, handler,
}).start();
}
constructor({ duration, name = "", handler }) {
if (typeof duration !== "number") {
throw new TypeError(
"duration must be a number"
);
}
if (duration <= 0) {
throw new RangeError(
"duration must be greater than 0"
);
}
this.#duration = duration;
this.#name = String(name);
if (handler) this.then(handler);
}
start() {
if (this.#state !== "ready") {
console.warn(
`Timer '${this.name}' cannot start `
+ `(state: ${this.#state})`
);
return this;
}
this.#state = "running";
this.#timeoutId = setTimeout(
() => this.#timeout(),
this.duration,
);
return this;
}
#timeout() {
this.#state = "completed";
this.#resolvers.resolve(this.duration);
}
cancel() {
if (this.#state !== "running") return;
clearTimeout(this.#timeoutId);
this.#state = "canceled";
this.#resolvers.reject(
new Error(`Timer '${this.name}' canceled.`)
);
}
get promise() { return this.#resolvers.promise; }
get state() { return this.#state; }
get name() { return this.#name; }
get duration() { return this.#duration; }
then(onFulfilled, onRejected) {
return this.#resolvers.promise.then(
onFulfilled, onRejected,
);
}
catch(onRejected) {
return this.#resolvers.promise.then(
undefined, onRejected,
);
}
finally(onSettled) {
return this.#resolvers.promise.finally(
onSettled,
);
}
}Stopwatch with laps
Tracks multiple checkpoints and returns the duration of each lap:
javascript
const sw = stopWatch();
await fetchData();
console.log(`Fetch: ${sw.lap()}ms`);
await processData();
console.log(`Process: ${sw.lap()}ms`);Implementing stopWatch()
Records performance.now() timestamps on each lap() call and returns the delta between consecutive checkpoints:
javascript
function stopWatch() {
const laps = [performance.now()];
return {
lap() {
laps.push(performance.now());
return laps.at(-1) - laps.at(-2);
},
};
}Simple elapsed timer
Minimal utility for measuring elapsed time from a starting point:
javascript
function createTimer() {
const start = performance.now();
return {
elapsed() {
return performance.now() - start;
},
};
}Elapsed timer with logging
Adds a logElapsed method that formats the duration:
javascript
const timer = useTimer();
await migrate();
timer.logElapsed("Migration complete");
// [1.2s] Migration completeImplementing useTimer()
Extends the simple elapsed timer with a logElapsed() method that formats durations for console output:
javascript
function useTimer() {
const start = performance.now();
return {
elapsed() {
return performance.now() - start;
},
logElapsed(...args) {
const time = `[${ms(this.elapsed())}]`;
console.log(time, ...args);
},
};
}Trade-offs
| Approach | Pros | Cons |
|---|---|---|
Timer class | Awaitable, cancelable | More code |
setTimeout | Simple, universal | Not promise-based |
stopWatch | Lap-based, chainable | No timestamps |
createTimer | Minimal | No formatting |
useTimer | Built-in logging | Needs ms() helper |