What's New
TaskTimer v4 is a 2026 modernization. The scheduling model is unchanged — one timer, many tasks, on tick intervals — but the surface around it is cleaner, more honest, and strongly typed, with some new sugar along the way: lead, task.data, typed events, coded errors, and silentErrors.
The Modernization
Section titled “The Modernization”- ESM-only, zero dependencies. TaskTimer is now a pure ES module (
"type": "module") with no runtime dependencies —eventemitter3was replaced by a small built-in, typed emitter that keeps the sameon/once/off/emitsurface. - Node 22+ and the browser. Runs in Node 22 or newer and in the browser via native ESM or any bundler.
- Drift-free precision everywhere. Timing now uses the monotonic
performance.now()in every environment, so browser precision is no longer subject to wall-clock drift. - Strongly typed. Built with TypeScript, event listeners are typed (you get an
ITaskTimerEvent, notany), and the package ships declaration maps.
New Sugar
Section titled “New Sugar”lead— run once at the leading edge. Setlead: trueon a task to run it once the moment the timer starts, in addition to its normal tick schedule, instead of waiting a full interval. A futurestartDatestill defers the first run.task.data— typed user data. Attach arbitrary data to a task and read it back astask.datain the callback and in event listeners.Taskis now generic (Task<TData>); type it viatimer.get<MyType>(id).timer.tasks— every task at once. A getter returning all tasks in insertion order. Read every id withtimer.tasks.map(t => t.id); usetimer.get(id)for a single lookup.silentErrors— surface unhandled task errors. A timer option (defaulttrue). Set it tofalseand a task error with notaskErrorlistener is re-thrown on the next turn instead of being swallowed. The timer keeps running either way.TaskTimerError+ErrorCode— coded errors. Every error the library throws is now aTaskTimerErrorcarrying a stable, machine-readablecode, so failures can be branched on without matching the message.
- A task’s
time.elapsedwas negative while the task was running; it now counts up live and freezes on completion, mirroring the timer’stime. - Invalid option values (
NaN/Infinity) no longer slip through — they fall back to their defaults instead of busy-looping or silently never running. - A task that throws on every run now honors
totalRunsinstead of running forever — an errored run counts toward completion. - Calling
start()from within a tick no longer leaves a second tick chain running (double-speed ticks).
Migrating from v3
Section titled “Migrating from v3”Named exports, no namespace
Section titled “Named exports, no namespace”TaskTimer is no longer a default export, and Event / State / Task are no longer namespaced under it. Everything is a named export.
// v3import TaskTimer from 'tasktimer';timer.on(TaskTimer.Event.TICK, /* … */);
// v4import { TaskTimer, Event, State } from 'tasktimer';timer.on(Event.TICK, /* … */);If your project isn’t ESM yet, set "type": "module" in your package.json (or use a bundler / a dynamic import()). See the ESM notice for the common cases.
Typed event payload
Section titled “Typed event payload”The event object was reshaped from { name, source, data } to { name, timer, task, error }. The related task is always event.task and the timer always event.timer — consistently, including on taskError.
// v3timer.on(TaskTimer.Event.TASK, event => { console.log(event.data.id, event.source.tickCount);});
// v4timer.on(Event.TASK, event => { console.log(event.task.id, event.timer.tickCount);});immediate → defer
Section titled “immediate → defer”The task option immediate was renamed to defer — it defers the callback to the next event-loop turn (the old name read as the opposite).
timer.add({ id: 'job', defer: true, callback: run }); // was: immediate: truetimer.get(id)
Section titled “timer.get(id)”get now returns Task | undefined (was typed Task, returned null) when no task matches, mirroring Map.get.
Browser usage
Section titled “Browser usage”v4 no longer ships a UMD <script> bundle (tasktimer.min.js). For the browser, bundle TaskTimer with your app using any modern bundler — Vite, esbuild, Rollup, or webpack.
Errors
Section titled “Errors”Errors thrown by the library are now TaskTimerError (still instanceof Error). Branch on err.code (an ErrorCode) if you handle them.
import { TaskTimer, TaskTimerError, ErrorCode } from 'tasktimer';
try { timer.add(existingTask);} catch (err) { if (err instanceof TaskTimerError && err.code === ErrorCode.DUPLICATE_TASK_ID) { // … }}The full list of changes is in the Changelog.