Format JSON in Node.js: JSON.stringify, util.inspect, and the BigInt trap
Formatting JSON in Node.js is one line you already know. The interesting questions are when to reach for util.inspect instead, what trips up JSON.stringify in real payloads, and how to write deterministic output to disk without a third-party library.
JSON.stringify with indent is enough
JSON.stringify(obj, null, 2) is the right answer for ninety percent of cases. The third argument is the indent. Pass 2 for two spaces, pass a tab string for tabs, pass nothing for the single-line wall of text that nobody can read in a log.
The second argument is the replacer. Pass null when you have no transform. Pass a function to redact secrets or coerce values. Pass an array of keys to whitelist properties. People forget the replacer exists and end up writing post-processing passes that the API was always going to handle.
When to skip util.inspect
util.inspect from node:util is for human eyes in a terminal. It uses single quotes, prints instead of expanding, colours output by default, and can show circular references as [Circular] rather than throwing. Great for development logs.
It is not JSON. The output cannot be parsed back. If you log inspect output and a downstream system tries to read it as JSON, the parse fails. Rule of thumb: util.inspect for the developer at the keyboard, JSON.stringify for any consumer that is not a human.
BigInt, circular refs, and Date
Three real pitfalls.
BigInt throws TypeError: Do not know how to serialize a BigInt. Coerce inside the replacer: (_, v) => typeof v === "bigint" ? v.toString() : v. Tag it on the wire if the consumer needs to know it was a BigInt.
Circular references throw Converting circular structure to JSON. Track seen objects with a Set inside the replacer and break the cycle. util.inspect tolerates them, JSON.stringify does not.
Date instances become ISO strings via toJSON. That is usually what you want, but if you need a Unix timestamp, override in the replacer or call getTime before stringifying.
Writing pretty JSON to disk
Use node:fs/promises with writeFile. For deterministic output sort the keys before stringifying. There is no built-in sort_keys flag in Node, so you build it with Object.keys().sort() and reduce. The example below produces the same bytes for the same input, every time, which is what makes diffs and content hashes useful.
Working example
javascriptimport { writeFile } from "node:fs/promises";
const config = {
port: 3000,
database: { host: "localhost", port: 5432 },
features: ["auth", "billing"],
createdAt: new Date(),
};
// Recursive sort to mimic Python's sort_keys=True
function sortKeys(value) {
if (Array.isArray(value)) return value.map(sortKeys);
if (value && typeof value === "object" && !(value instanceof Date)) {
return Object.keys(value)
.sort()
.reduce((acc, k) => {
acc[k] = sortKeys(value[k]);
return acc;
}, {});
}
return value;
}
const formatted = JSON.stringify(sortKeys(config), null, 2);
await writeFile("config.json", formatted, "utf8"); Just need the result?
When you have a payload from a Node service and you want to inspect it visually without dropping into a REPL, paste it into the JSON formatter on aldeacode.com. It runs entirely in the browser, never uploads, and gives you valid pretty JSON that round-trips through any parser.
Open JSON Formatter and Validator →Frequently asked questions
Should I prefer node:fs/promises or the callback API?
Promises. The callback API is still supported but the promises module is the modern default in Node 18+ and works cleanly with await at the top level when type:module is set in package.json.
Is util.inspect.colors safe to use in production logs?
Only if the destination is a TTY. Pipe a coloured stream into a log aggregator and you get ANSI escape codes mixed into the indexed text. Detect with process.stdout.isTTY and disable colour when false.
How do I stream large objects without loading everything?
JSON.stringify builds the full string in memory. For multi-gigabyte payloads use a streaming serialiser like JSONStream or write each top-level item with a manual JSON.stringify and a comma between calls.