util.format()

Builds a string from a printf-style format and its arguments, the way console.log does.

Signature
util.format(format[, ...args])
  • Added in Node.js v0.5.3

Parameters

ParameterTypeDescription
$formatstringA printf-like format string containing zero or more specifiers (%s, %d, %i, %f, %j, %o, %O, %c, %%).
$...argsoptionalanyValues substituted into the specifiers, in order. Extra arguments are inspected with util.inspect() and appended.

Return value

stringThe formatted string: each specifier replaced by its argument, with any leftover arguments inspected and appended, space separated.

Errors

Does not throw for ordinary input; a getter invoked while inspecting a value may throw, and that error is reported inline instead.

Changelog

  • PHP v12.11.0The %c specifier is now ignored and consumes its argument without output.
  • PHP v12.0.0The first argument is only treated as a format string if it actually contains specifiers, and quotes are no longer added around a non-string first argument.
  • PHP v11.4.0%d, %f and %i handle Symbols properly, and %o went back to a default depth of 4.
  • PHP v10.12.0%d and %i accept BigInt values.
  • PHP v8.4.0The %o and %O specifiers were added.

util.format() is the engine behind console.log(): when you write console.log('%s took %dms', name, ms), this is the function doing the substitution. Calling it directly gives you the same string without writing it anywhere.

When to reach for it

Use it when you need the formatted text as a value — building a log line for a transport, composing an Error message, or writing a logger that must match console.log's output byte for byte. For anything user-facing, template literals are clearer; util.format() earns its place when you want util.inspect()'s object rendering rather than String(obj)'s [object Object].

What trips people up

  • A lone string is returned untouched. With no further arguments there is nothing to substitute, so Node skips the parser entirely — even %% survives as two characters.
  • Extra arguments are appended, not ignored. Leftovers are inspected and joined with spaces, which is why a stray argument shows up as { a: 1 } at the end of the line.
  • Unmatched specifiers stay literal. util.format('%s:%s', 'foo') returns 'foo:%s'.
  • %d is not parseInt. %d is Number(), %i is parseInt(value, 10), %f is parseFloat(value) — so '42.9px' gives NaN, 42 and 42.9 respectively.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Build a log line for a file or network transport that must match console.log's output exactly.
  • Compose an Error message that renders an offending object with util.inspect instead of [object Object].
  • Implement a custom logger whose info()/error() signatures accept the same specifiers users already know.

Frequently asked questions

Why did util.format not replace my %% with a single percent sign?

Because a format string passed with no additional arguments is returned unchanged — Node skips the substitution pass entirely, so `util.format('100%% done')` yields `'100%% done'`. Pass at least one further argument, or hard-code the single `%` yourself.

What is the difference between %d, %i and %f in util.format?

`%d` applies `Number()`, `%i` applies `parseInt(value, 10)` and `%f` applies `parseFloat(value)`. For the string `'42.9px'` that gives `NaN`, `42` and `42.9`. `%d` and `%i` also accept BigInt but reject Symbol; `%f` accepts everything but Symbol.

What is the difference between %o and %O?

`%o` inspects with `showHidden: true` and `showProxy: true`, so non-enumerable properties such as an array's `length` and any Proxy internals are shown. `%O` inspects with the default options and shows only ordinary enumerable properties.

More examples

What each specifier actually does

The first line is the distinction people get wrong most often: `%d` runs `Number()` and produces `NaN` for `'42.9px'`, while `%i` and `%f` run `parseInt` and `parseFloat` and happily stop at the unit suffix. `%j` is the only specifier that produces JSON, and it substitutes the string `'[Circular]'` rather than throwing on a cycle. `%o` is `%O` plus `showHidden: true` and `showProxy: true`, which is why the array's `[length]` appears.

Example · javascript
const util = require('node:util');

const value = '42.9px';
console.log(util.format('%s|%d|%i|%f', value, value, value, value));
// 42.9px|NaN|42|42.9

// BigInt and Symbol survive %s and %d, but not %j.
console.log(util.format('%s %d', 10n, 10n));      // 10n 10n
console.log(util.format('%s', Symbol('x')));       // Symbol(x)

// %j is JSON, and is circular-safe.
const circular = { name: 'root' };
circular.self = circular;
console.log(util.format('%j', circular));          // {"name":"root","self":"[Circular]"}

// %o shows hidden properties and proxies; %O does not.
console.log(util.format('%o', [1, 2]));            // [ 1, 2, [length]: 2 ]
console.log(util.format('%O', [1, 2]));            // [ 1, 2 ]

// A user toString() wins over inspection for %s.
console.log(util.format('%s', { toString: () => 'custom' })); // custom

Controlling inspection depth with formatWithOptions

`util.format()` hard-codes its inspect options, so deep objects truncate to `[Object]` with no way to override it. `util.formatWithOptions()` (added in v10.0.0) takes an inspect options object first and applies it to every `%o`/`%O`/`%s` substitution, which is the hook a custom logger needs to widen depth or enable colour.

Example · javascript
const util = require('node:util');

const tree = { a: { b: { c: { d: 1 } } } };

// util.format() gives you no control over the inspect options.
console.log(util.format('%O', tree));
// { a: { b: { c: [Object] } } }

// util.formatWithOptions() takes them as its first argument.
console.log(util.formatWithOptions({ depth: 0 }, '%O', tree));
// { a: [Object] }

console.log(util.formatWithOptions({ depth: null }, '%O', tree));
// { a: { b: { c: { d: 1 } } } }

// Colours too, for a logger writing to a known-TTY stream.
console.log(util.formatWithOptions({ colors: true }, '%O', { n: 1 }));

A minimal console-compatible logger

Because the logger delegates to `util.format()`, calls behave identically to `console.log()` — specifiers substitute, extra arguments append, objects render through `util.inspect()` rather than `[object Object]`, and an `Error` argument contributes its stack. That equivalence is the main reason to use `util.format()` over template literals in logging code.

Example · javascript
const util = require('node:util');
const fs = require('node:fs');

function createLogger(stream) {
  const write = (level, args) => {
    // util.format applies exactly console.log's rules.
    const message = util.format(...args);
    stream.write(`[${level}] ${message}\n`);
  };

  return {
    info: (...args) => write('info', args),
    error: (...args) => write('error', args),
  };
}

const log = createLogger(process.stdout);

log.info('user %s connected from %s', 'ada', '10.0.0.1');
log.info('state:', { retries: 3, tags: ['a', 'b'] });
log.error('failed after %d attempts', 3, new RangeError('too many'));

// [info] user ada connected from 10.0.0.1
// [info] state: { retries: 3, tags: [ 'a', 'b' ] }
// [error] failed after 3 attempts RangeError: too many
//     at ...

Discussion

  • Be the first to comment on this lesson.