pretty-ms
One function that turns a millisecond count into a string a person can read: 1337000000 becomes '15d 11h 23m 20s', 1337 becomes '1.3s', 133 becomes '133ms'. It takes a number or a bigint and a small options object that controls how many units to show, whether to spell them out ('15 days 11 hours'), whether to use colon notation like a stopwatch ('1:35.5'), and how many decimals to keep. It is the thing you reach for when a CLI needs to print how long a build took, or a dashboard needs to show job runtime, and you do not want to hand-roll the divide-and-pad arithmetic again. The whole package is about 1 KB gzipped with one dependency, parse-ms, from the same author.
The correct answer for English duration strings in a Node CLI or build tool: tiny, well-specified, and it has thought about the edge cases you have not. Skip it if you need translated output or still ship CommonJS.
Use it if
- You print elapsed times in a CLI or log line and want consistent output like '2m 30s' without writing modulo arithmetic in every project
- You need a stopwatch or media-player style display: colonNotation gives you '1:35.5' with correct zero padding
- You measure with process.hrtime.bigint() and want sub-millisecond output: it accepts bigint input and can format microseconds and nanoseconds
- You want a progress indicator whose width does not jitter: keepDecimalsOnWholeSeconds and subSecondsAsDecimals exist specifically for that
- You ship CommonJS: the package is ESM only since v8, so require('pretty-ms') fails and you need a dynamic import() or a bundler step
- You need translated output: every unit label is hardcoded English ('days', 'hours'), there is no locale option, and Intl.DurationFormat covers that natively on runtimes that support it
- You only ever print one shape, like '3.2s': that is a two-line helper, and adding a dependency plus its transitive parse-ms is not worth it
- You need calendar-accurate months or years: the FAQ says it formats durations rather than calendar dates, and the year unit it does emit is a flat 365-day division, so long spans drift
- Your durations come from date subtraction and you need the printed values to add up: rounding is per value, so the formatted difference of two formatted numbers can look off by one tenth
Setup reality
npm install pretty-ms, import the default export, call it. The only real friction is module format and Node version: package.json declares type module with an exports map and engines node >=18, so CommonJS consumers get ERR_REQUIRE_ESM and have to await import('pretty-ms') or transpile. Jest setups without ESM support choke on it for the same reason. Types are bundled, there are no peer dependencies and no native builds, and the one runtime dependency is parse-ms. Passing NaN or Infinity throws a TypeError rather than returning a placeholder, so validate before formatting values that come from timers that never fired.
Patterns
Format a duration in millisecondsbasic-format
import prettyMilliseconds from 'pretty-ms';
prettyMilliseconds(1337000000); //=> '15d 11h 23m 20s'
prettyMilliseconds(1337); //=> '1.3s'
prettyMilliseconds(133); //=> '133ms'
prettyMilliseconds(0); //=> '0ms'Sub-second values print as milliseconds by default; seconds carry one decimal place unless you change secondsDecimalDigits.
Print how long an operation tooktime-an-operation
const start = Date.now();
await buildProject();
console.log(`Built in ${prettyMilliseconds(Date.now() - start)}`);
// or between two dates
prettyMilliseconds(new Date(2014, 0, 1, 10, 40) - new Date(2014, 0, 1, 10, 5)); //=> '35m'Date subtraction yields a plain number of milliseconds, which is exactly the input shape this expects.
Show only the largest unitcompact-single-unit
prettyMilliseconds(1337, {compact: true}); //=> '1s'
prettyMilliseconds(5400000, {compact: true}); //=> '1h'
// two units instead of one:
prettyMilliseconds(5400000, {unitCount: 2}); //=> '1h 30m'compact is unitCount 1 plus zeroed decimals, and it overrides unitCount if you pass both.
Spell out the unit namesverbose-units
prettyMilliseconds(1335669000, {verbose: true});
//=> '15 days 11 hours 1 minute 9 seconds'Pluralization is handled per unit, but the words are English only; there is no locale option.
Render stopwatch style with colonscolon-notation
prettyMilliseconds(95500, {colonNotation: true}); //=> '1:35.5'
prettyMilliseconds(1000, {colonNotation: true}); //=> '0:01'
prettyMilliseconds(18300000, {colonNotation: true}); //=> '5:05:00'colonNotation forces compact, verbose, separateMilliseconds, and formatSubMilliseconds off, and always shows at least minutes.
Control decimal places on secondsdecimal-control
prettyMilliseconds(1337, {secondsDecimalDigits: 0}); //=> '1s'
prettyMilliseconds(13000, {keepDecimalsOnWholeSeconds: true}); //=> '13.0s'
prettyMilliseconds(900, {subSecondsAsDecimals: true}); //=> '0.9s'keepDecimalsOnWholeSeconds and subSecondsAsDecimals both exist to stop the string width from jumping in a live-updating progress line.
Format nanosecond timings from hrtimesub-millisecond-precision
const start = process.hrtime.bigint();
doWork();
const ns = process.hrtime.bigint() - start;
prettyMilliseconds(Number(ns) / 1e6, {formatSubMilliseconds: true});
//=> e.g. '100ms 400\u00b5s 80ns'formatSubMilliseconds splits microseconds and nanoseconds into their own units instead of folding them into a decimal.
Pass a bigint for very large or exact valuesbigint-input
prettyMilliseconds(1337000000n); //=> '15d 11h 23m 20s'bigint input avoids float rounding on huge spans; mixing bigint and number arithmetic before the call is what usually breaks, not the call itself.
Suppress years, days, or secondshide-units
const ms = 31_795_305_000; // about 1y 3d 5h 1m 45s
prettyMilliseconds(ms, {hideYear: true}); //=> '368d 5h 1m 45s'
prettyMilliseconds(ms, {hideYearAndDays: true}); //=> '8837h 1m 45s'
prettyMilliseconds(ms, {hideSeconds: true}); //=> '1y 3d 5h 1m'The year unit is computed as days divided by 365, so it is a rough bucket, not a calendar year. Use hideYear when that approximation would mislead.
Handle negative and non-finite inputnegative-and-invalid
prettyMilliseconds(-1500); //=> '-1.5s'
try {
prettyMilliseconds(Number.NaN);
} catch (error) {
// TypeError: Expected a finite number or bigint
}Negative durations get a leading minus sign. NaN and Infinity throw, so guard timers that may never have started.
Use it from CommonJScommonjs-import
// index.cjs
async function main() {
const {default: prettyMilliseconds} = await import('pretty-ms');
console.log(prettyMilliseconds(4242));
}
main();The package is ESM only from v8 onward, so require() throws ERR_REQUIRE_ESM. Dynamic import works from CJS on Node 18+; older code can pin pretty-ms@7.
Format a column of durations consistentlytable-of-durations
const jobs = [{name: 'lint', ms: 812}, {name: 'test', ms: 94_300}, {name: 'build', ms: 3_600_000}];
for (const job of jobs) {
const time = prettyMilliseconds(job.ms, {unitCount: 2, secondsDecimalDigits: 0});
console.log(job.name.padEnd(8), time.padStart(9));
}Fixing unitCount and dropping decimals keeps column widths stable; the library does not pad output for you.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| humanize-duration | npm | You need the same output translated into other languages, with configurable unit lists |
| ms | npm | You want the tiny two-way helper that both parses '2 days' and prints short forms, at the cost of only one unit at a time |
| date-fns | npm | You already use date-fns and want formatDuration plus locale support from the same package |
| luxon | npm | Your durations come from real calendar math and you want Duration objects rather than a formatting function |