Standard JSON.stringify() crashes when encountering circular references, a common issue in complex state management, logging, and error reporting. The packages fast-safe-stringify, json-stringify-safe, safe-json-stringify, and safe-stable-stringify solve this by detecting cycles and replacing them with placeholder strings like "[Circular]". Beyond safety, they differ significantly in performance, memory usage, and whether they guarantee a consistent key order (determinism), which is critical for hashing, caching, and testing.
In JavaScript, JSON.stringify() is the standard tool for converting objects to strings. However, it has a fatal flaw: it throws a TypeError if the object graph contains a circular reference. This happens frequently in real-world apps—think of DOM nodes, complex error objects with cause chains, or mutually referenced state models.
To fix this, developers turn to "safe" stringifiers. But not all are built the same. Some prioritize raw speed, others guarantee that the output string is always identical (deterministic), and some are simply outdated. Let's break down how fast-safe-stringify, json-stringify-safe, safe-json-stringify, and safe-stable-stringify actually work under the hood.
All four packages solve the crash problem by detecting cycles and replacing the circular link with a placeholder string (usually "[Circular]"). However, the way they detect these cycles impacts performance.
fast-safe-stringify uses a highly optimized approach that avoids heavy memory overhead. It detects cycles quickly without slowing down the serialization of normal objects.
import stringify from 'fast-safe-stringify';
const obj = { name: 'Alice' };
obj.self = obj; // Circular reference
// Output: '{"name":"Alice","self":"[Circular]"}'
console.log(stringify(obj));
json-stringify-safe was the pioneer in this space. It works reliably but uses an older detection method that can be slower on deeply nested structures.
import stringify from 'json-stringify-safe';
const obj = { name: 'Alice' };
obj.self = obj;
// Output: '{"name":"Alice","self":"[Circular]"}'
console.log(stringify(obj));
safe-json-stringify provides similar safety guarantees. It traverses the object and swaps out circular refs, ensuring your app doesn't crash.
import stringify from 'safe-json-stringify';
const obj = { name: 'Alice' };
obj.self = obj;
// Output: '{"name":"Alice","self":"[Circular]"}'
console.log(stringify(obj));
safe-stable-stringify also handles cycles safely, but it does extra work to ensure the order of keys remains consistent, which we will discuss in the next section.
import stringify from 'safe-stable-stringify';
const obj = { name: 'Alice' };
obj.self = obj;
// Output: '{"name":"Alice","self":"[Circular]"}'
console.log(stringify(obj));
When serializing large logs or high-frequency data, speed matters. The trade-off here is usually between "fastest possible" and "deterministic output."
fast-safe-stringify lives up to its name. It is currently the fastest option for most use cases where key order doesn't matter. It mimics the native JSON.stringify behavior closely, preserving the insertion order of keys while adding cycle detection.
// Best for high-volume logging where order doesn't matter
import stringify from 'fast-safe-stringify';
const largeData = generateHugeObject();
const json = stringify(largeData); // Minimal overhead
json-stringify-safe and safe-json-stringify are noticeably slower. They were written years ago when JavaScript engines were less optimized for these patterns. In modern benchmarks, they often lag behind fast-safe-stringify by a significant margin.
// Legacy performance profile
import stringify from 'json-stringify-safe';
const json = stringify(largeData); // Higher CPU usage compared to modern libs
safe-stable-stringify sacrifices some raw speed to guarantee stability. Sorting keys or enforcing a specific traversal order takes extra computation. If you need stability, this cost is worth it; if you just need speed, it might be overkill.
// Pays a small performance tax for deterministic output
import stringify from 'safe-stable-stringify';
const json = stringify(largeData); // Slower than fast-safe-stringify, but stable
This is the most critical architectural difference. Native JSON.stringify outputs keys in the order they were added to the object. If you create an object with keys {a, b} in one run and {b, a} in another (perhaps due to refactoring or different code paths), the resulting JSON strings will be different, even if the data is the same.
This breaks:
fast-safe-stringify, json-stringify-safe, and safe-json-stringify do NOT guarantee stable key ordering. They preserve insertion order. If your code creates objects differently, your strings will differ.
// UNSTABLE: Output depends on how 'obj' was constructed
import stringify from 'fast-safe-stringify';
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 2, a: 1 };
console.log(stringify(obj1)); // '{"a":1,"b":2}'
console.log(stringify(obj2)); // '{"b":2,"a":1}' -> Different string!
safe-stable-stringify is the only one in this list designed specifically to solve this. It recursively sorts keys or uses a deterministic traversal strategy so that equivalent objects always produce the exact same string, regardless of creation order.
// STABLE: Output is always sorted/consistent
import stringify from 'safe-stable-stringify';
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 2, a: 1 };
console.log(stringify(obj1)); // '{"a":1,"b":2}'
console.log(stringify(obj2)); // '{"a":1,"b":2}' -> Identical string!
Choosing a library isn't just about features; it's about long-term viability.
json-stringify-safe is effectively deprecated for new work. It hasn't seen significant updates in years. While it still works, relying on unmaintained packages for core infrastructure like serialization introduces unnecessary risk.
safe-json-stringify suffers from similar issues. It is a simple utility that hasn't evolved to match modern engine optimizations.
fast-safe-stringify is actively maintained and widely adopted in the ecosystem (used by major logging libraries). It strikes the best balance for general use.
safe-stable-stringify is also actively maintained and has become the standard for tools requiring reproducibility, such as build tools, testing frameworks, and cryptographic utilities.
You are logging thousands of request objects per second. Some objects might have circular refs (e.g., from Express request contexts). You need maximum throughput.
fast-safe-stringifylogger.info(fastSafeStringify(reqObject));
You are creating a cache key based on a configuration object. If the string representation changes due to key ordering, the cache lookup will fail, causing performance issues.
safe-stable-stringify{ port: 80, host: 'localhost' } produces the same hash as { host: 'localhost', port: 80 }.const cacheKey = crypto.createHash('md5')
.update(safeStableStringify(config))
.digest('hex');
You are fixing a bug in a 7-year-old Node.js service that already uses json-stringify-safe.
json-stringify-safe (or refactor if possible)// Existing legacy code
const payload = jsonStringifySafe(complexErrorObject);
| Feature | fast-safe-stringify | json-stringify-safe | safe-json-stringify | safe-stable-stringify |
|---|---|---|---|---|
| Circular Safety | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| Performance | 🚀 Very Fast | 🐢 Slow | 🐢 Slow | ⚡ Fast (with overhead) |
| Deterministic Keys | ❌ No (Insertion Order) | ❌ No | ❌ No | ✅ Yes (Sorted/Stable) |
| Maintenance Status | 🟢 Active | 🔴 Inactive/Legacy | 🟡 Low Activity | 🟢 Active |
| Best Use Case | Logging, APIs | Legacy Support | Legacy Support | Hashing, Caching, Tests |
For most modern frontend and Node.js applications, fast-safe-stringify is the default winner. It prevents crashes without slowing down your app, making it perfect for logging and data transport where exact key order doesn't impact logic.
However, if your architecture relies on reproducibility—such as generating content hashes for caching, signing data, or running snapshot tests—you must use safe-stable-stringify. The slight performance cost is a necessary trade-off to ensure your system behaves predictably.
Avoid json-stringify-safe and safe-json-stringify for new projects. They represent an older generation of solutions that have been surpassed by faster, more robust alternatives. Stick to the actively maintained tools to keep your dependency tree healthy.
Pick safe-stable-stringify when you need guaranteed deterministic output (stable key ordering) alongside circular reference safety. This is essential for generating consistent hashes, comparing objects in tests, or caching mechanisms where the string representation must remain identical across different runs, even if object key insertion order varies.
Choose fast-safe-stringify for general-purpose applications where performance is the top priority and key order does not matter. It is the fastest option for large datasets and is actively maintained, making it the default choice for high-throughput logging or API responses where deterministic output is not required.
Avoid json-stringify-safe for new projects. While it was the original solution for circular references, it is no longer actively maintained and is significantly slower than modern alternatives. Only use it if you are maintaining a legacy codebase that already depends on it and cannot tolerate a refactor.
Select safe-json-stringify only if you are working in a very specific legacy environment or need a extremely minimal footprint without modern optimizations. Like json-stringify-safe, it lacks the performance benefits and deterministic features of newer packages and is generally not recommended for modern architecture.
Safe, deterministic and fast serialization alternative to JSON.stringify. Zero dependencies. ESM and CJS. 100% coverage.
Gracefully handles circular structures and bigint instead of throwing.
Optional custom circular values, deterministic behavior or strict JSON compatibility check.
The same as JSON.stringify.
value {any}replacer {string[]|function|null}space {number|string}const stringify = require('safe-stable-stringify')
const bigint = { a: 0, c: 2n, b: 1 }
stringify(bigint)
// '{"a":0,"b":1,"c":2}'
JSON.stringify(bigint)
// TypeError: Do not know how to serialize a BigInt
const circular = { b: 1, a: 0 }
circular.circular = circular
stringify(circular)
// '{"a":0,"b":1,"circular":"[Circular]"}'
JSON.stringify(circular)
// TypeError: Converting circular structure to JSON
stringify(circular, ['a', 'b'], 2)
// {
// "a": 0,
// "b": 1
// }
bigint {boolean} If true, bigint values are converted to a number. Otherwise
they are ignored. Default: true.circularValue {string|null|undefined|ErrorConstructor} Defines the value for
circular references. Set to undefined, circular properties are not
serialized (array entries are replaced with null). Set to Error, to throw
on circular references. Default: '[Circular]'.deterministic {boolean|function} If true or a Array#sort(comparator)
comparator method, guarantee a deterministic key order instead of relying on
the insertion order. Default: true.maximumBreadth {number} Maximum number of entries to serialize per object
(at least one). The serialized output contains information about how many
entries have not been serialized. Ignored properties are counted as well
(e.g., properties with symbol values). Using the array replacer overrules this
option. Default: InfinitymaximumDepth {number} Maximum number of object nesting levels (at least 1)
that will be serialized. Objects at the maximum level are serialized as
'[Object]' and arrays as '[Array]'. Default: Infinitystrict {boolean} Instead of handling any JSON value gracefully, throw an
error in case it may not be represented as JSON (functions, NaN, ...).
Circular values and bigint values throw as well in case either option is not
explicitly defined. Sets and Maps are not detected as well as Symbol keys!
Default: falseimport { configure } from 'safe-stable-stringify'
const stringify = configure({
bigint: true,
circularValue: 'Magic circle!',
deterministic: false,
maximumDepth: 1,
maximumBreadth: 4
})
const circular = {
bigint: 999_999_999_999_999_999n,
typed: new Uint8Array(3),
deterministic: "I don't think so",
}
circular.circular = circular
circular.ignored = true
circular.alsoIgnored = 'Yes!'
const stringified = stringify(circular, null, 4)
console.log(stringified)
// {
// "bigint": 999999999999999999,
// "typed": "[Object]",
// "deterministic": "I don't think so",
// "circular": "Magic circle!",
// "...": "2 items not stringified"
// }
const throwOnCircular = configure({
circularValue: Error
})
throwOnCircular(circular);
// TypeError: Converting circular structure to JSON
[Circular] (configurable).Number(5)) are not unboxed and are handled as
regular object.Those are the only differences to JSON.stringify(). This is a side effect free
variant and toJSON, replacer and the spacer work the same as
with JSON.stringify().
Currently this is by far the fastest known stable (deterministic) stringify implementation. This is especially important for big objects and TypedArrays.
(Dell Precision 5540, i7-9850H CPU @ 2.60GHz, Node.js 16.11.1)
simple: simple object x 3,463,894 ops/sec ±0.44% (98 runs sampled)
simple: circular x 1,236,007 ops/sec ±0.46% (99 runs sampled)
simple: deep x 18,942 ops/sec ±0.41% (93 runs sampled)
simple: deep circular x 18,690 ops/sec ±0.72% (96 runs sampled)
replacer: simple object x 2,664,940 ops/sec ±0.31% (98 runs sampled)
replacer: circular x 1,015,981 ops/sec ±0.09% (99 runs sampled)
replacer: deep x 17,328 ops/sec ±0.38% (97 runs sampled)
replacer: deep circular x 17,071 ops/sec ±0.21% (98 runs sampled)
array: simple object x 3,869,608 ops/sec ±0.22% (98 runs sampled)
array: circular x 3,853,943 ops/sec ±0.45% (96 runs sampled)
array: deep x 3,563,227 ops/sec ±0.20% (100 runs sampled)
array: deep circular x 3,286,475 ops/sec ±0.07% (100 runs sampled)
indentation: simple object x 2,183,162 ops/sec ±0.66% (97 runs sampled)
indentation: circular x 872,538 ops/sec ±0.57% (98 runs sampled)
indentation: deep x 16,795 ops/sec ±0.48% (93 runs sampled)
indentation: deep circular x 16,443 ops/sec ±0.40% (97 runs sampled)
Comparing safe-stable-stringify with known alternatives:
fast-json-stable-stringify x 18,765 ops/sec ±0.71% (94 runs sampled)
json-stable-stringify x 13,870 ops/sec ±0.72% (94 runs sampled)
fast-stable-stringify x 21,343 ops/sec ±0.33% (95 runs sampled)
faster-stable-stringify x 17,707 ops/sec ±0.44% (97 runs sampled)
json-stringify-deterministic x 11,208 ops/sec ±0.57% (98 runs sampled)
fast-safe-stringify x 21,460 ops/sec ±0.75% (99 runs sampled)
this x 30,367 ops/sec ±0.39% (96 runs sampled)
The fastest is this
The fast-safe-stringify comparison uses the modules stable implementation.
Sponsored by MaibornWolff and nearForm
MIT