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.
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.
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.
Safe and fast serialization alternative to JSON.stringify.
Gracefully handles circular structures instead of throwing in most cases. It could return an error string if the circular object is too complex to analyze, e.g. in case there are proxies involved.
Provides a deterministic ("stable") version as well that will also gracefully handle circular structures. See the example below for further information.
The same as JSON.stringify.
stringify(value[, replacer[, space[, options]]])
const safeStringify = require('fast-safe-stringify')
const o = { a: 1 }
o.o = o
console.log(safeStringify(o))
// '{"a":1,"o":"[Circular]"}'
console.log(JSON.stringify(o))
// TypeError: Converting circular structure to JSON
function replacer(key, value) {
console.log('Key:', JSON.stringify(key), 'Value:', JSON.stringify(value))
// Remove the circular structure
if (value === '[Circular]') {
return
}
return value
}
// those are also defaults limits when no options object is passed into safeStringify
// configure it to lower the limit.
const options = {
depthLimit: Number.MAX_SAFE_INTEGER,
edgesLimit: Number.MAX_SAFE_INTEGER
};
const serialized = safeStringify(o, replacer, 2, options)
// Key: "" Value: {"a":1,"o":"[Circular]"}
// Key: "a" Value: 1
// Key: "o" Value: "[Circular]"
console.log(serialized)
// {
// "a": 1
// }
Using the deterministic version also works the same:
const safeStringify = require('fast-safe-stringify')
const o = { b: 1, a: 0 }
o.o = o
console.log(safeStringify(o))
// '{"b":1,"a":0,"o":"[Circular]"}'
console.log(safeStringify.stableStringify(o))
// '{"a":0,"b":1,"o":"[Circular]"}'
console.log(JSON.stringify(o))
// TypeError: Converting circular structure to JSON
A faster and side-effect free implementation is available in the [safe-stable-stringify][] module. However it is still considered experimental due to a new and more complex implementation.
[Circular] - when same reference is found[...] - when some limit from options object is reachedIn general the behavior is identical to JSON.stringify. The replacer
and space options are also available.
A few exceptions exist to JSON.stringify while using toJSON or
replacer:
Manipulating a circular structure of the passed in value in a toJSON or the
replacer is not possible! It is possible for any other value and property.
In case a circular structure is detected and the replacer is used it
will receive the string [Circular] as the argument instead of the circular
object itself.
Manipulating the input object either in a toJSON or the replacer
function will not have any effect on the output. The output entirely relies on
the shape the input value had at the point passed to the stringify function!
In case a circular structure is detected and the replacer is used it
will receive the string [Circular] as the argument instead of the circular
object itself.
A side effect free variation without these limitations can be found as well
(safe-stable-stringify). It is also faster than the current
implementation. It is still considered experimental due to a new and more
complex implementation.
Although not JSON, the Node.js util.inspect method can be used for similar
purposes (e.g. logging) and also handles circular references.
Here we compare fast-safe-stringify with some alternatives:
(Lenovo T450s with a i7-5600U CPU using Node.js 8.9.4)
fast-safe-stringify: simple object x 1,121,497 ops/sec ±0.75% (97 runs sampled)
fast-safe-stringify: circular x 560,126 ops/sec ±0.64% (96 runs sampled)
fast-safe-stringify: deep x 32,472 ops/sec ±0.57% (95 runs sampled)
fast-safe-stringify: deep circular x 32,513 ops/sec ±0.80% (92 runs sampled)
util.inspect: simple object x 272,837 ops/sec ±1.48% (90 runs sampled)
util.inspect: circular x 116,896 ops/sec ±1.19% (95 runs sampled)
util.inspect: deep x 19,382 ops/sec ±0.66% (92 runs sampled)
util.inspect: deep circular x 18,717 ops/sec ±0.63% (96 runs sampled)
json-stringify-safe: simple object x 233,621 ops/sec ±0.97% (94 runs sampled)
json-stringify-safe: circular x 110,409 ops/sec ±1.85% (95 runs sampled)
json-stringify-safe: deep x 8,705 ops/sec ±0.87% (96 runs sampled)
json-stringify-safe: deep circular x 8,336 ops/sec ±2.20% (93 runs sampled)
For stable stringify comparisons, see the performance benchmarks in the
safe-stable-stringify readme.
Whether fast-safe-stringify or alternatives are used: if the use case
consists of deeply nested objects without circular references the following
pattern will give best results.
Shallow or one level nested objects on the other hand will slow down with it.
It is entirely dependant on the use case.
const stringify = require('fast-safe-stringify')
function tryJSONStringify (obj) {
try { return JSON.stringify(obj) } catch (_) {}
}
const serializedString = tryJSONStringify(deep) || stringify(deep)
Sponsored by nearForm
MIT