How to Fix: console logging circular references in Server Components causes error
Logging a circular object inside a Next.js Server Component can crash rendering because the value sent through the server-side logging pipeline is often inspected, serialized, or transformed in a way that does not tolerate self-references. In the linked reproduction, calling console.log() on a complex class instance with circular references triggers an error during Server Component execution instead of producing a safe debug print.
Problem Overview
This issue appears in Next.js Server Components when you log an object that contains a circular structure, such as:
- a class instance referencing itself
- a nested object graph with parent-child back references
- framework objects that contain internal cycles
In plain Node.js, some circular values can be logged safely because Node’s inspector knows how to label cycles. But in a Server Component environment, the runtime may wrap logging, capture output for overlays, or serialize values for transport and diagnostics. That extra processing is where the failure happens.
If you want to inspect the reproduction, use the GitHub reproduction repository.
Understanding the Root Cause
The bug is rooted in the mismatch between runtime inspection and serialization safety.
A circular reference means an object eventually points back to itself:
const obj = {};
obj.self = obj;
That structure is valid JavaScript, but it is not valid for naive JSON serialization:
JSON.stringify(obj); // Throws: Converting circular structure to JSON
In Server Components, console.log() is not always just a direct terminal print. Depending on the framework internals, development overlay, React Server Components pipeline, or instrumentation hooks, the logged arguments may be:
- stringified for transport
- inspected by custom formatters
- captured for error reporting
- replayed in development tooling
If any of those steps uses a serializer that does not track visited objects, the framework throws when it encounters a cycle. That is why the same object may log fine in one Node context but fail inside a Next.js Server Component render path.
In short: the console call is not the real problem; the framework’s handling of logged values is.
Step-by-Step Solution
The safest fix is to avoid logging raw complex instances from Server Components. Instead, log a sanitized snapshot that removes cycles and strips non-essential internals.
1. Create a safe serializer
export function safeSerialize(value) {
const seen = new WeakSet();
return JSON.stringify(
value,
(key, currentValue) => {
if (typeof currentValue === 'object' && currentValue !== null) {
if (seen.has(currentValue)) {
return '[Circular]';
}
seen.add(currentValue);
}
if (typeof currentValue === 'function') {
return `[Function: ${currentValue.name || 'anonymous'}]`;
}
if (currentValue instanceof Error) {
return {
name: currentValue.name,
message: currentValue.message,
stack: currentValue.stack,
};
}
return currentValue;
},
2
);
}
2. Use it instead of logging the raw object
import { safeSerialize } from './safe-serialize';
export default async function Page() {
const instance = createComplexClassInstance();
console.log(safeSerialize(instance));
return <div>Hello</div>;
}
This converts circular references into a safe marker like [Circular] instead of crashing the render.
3. Prefer targeted logging over full object dumps
For production-grade debugging, log only the fields you actually need:
console.log({
id: instance.id,
type: instance.constructor?.name,
status: instance.status,
});
This is usually better than dumping an entire object graph, especially in server-rendered code.
4. Add a reusable safe logger helper
export function safeLog(label, value) {
try {
console.log(label, safeSerialize(value));
} catch (error) {
console.log(label, '[Unserializable value]', {
message: error instanceof Error ? error.message : String(error),
});
}
}
safeLog('server component payload', instance);
5. For class instances, expose a debug shape
If you own the class, define a method that returns a serializable summary:
class UserGraph {
constructor(user, parent = null) {
this.user = user;
this.parent = parent;
}
toDebugJSON() {
return {
userId: this.user?.id,
name: this.user?.name,
hasParent: Boolean(this.parent),
};
}
}
console.log(instance.toDebugJSON());
This avoids leaking internals while keeping logs readable.
6. If needed, use Node inspection for local debugging only
import util from 'node:util';
console.log(
util.inspect(instance, {
depth: 4,
colors: false,
})
);
This can help during local development, but it is still safer to avoid depending on framework logging wrappers in Server Components. A string returned by util.inspect() is generally much safer than logging the live object directly.
Recommended practical fix
If you need one reliable pattern, use this:
import util from 'node:util';
export function debugServerValue(label, value) {
console.log(label, util.inspect(value, { depth: 5, colors: false }));
}
Because util.inspect() returns a string, the framework is much less likely to choke on circular references later in the pipeline.
Common Edge Cases
- Error objects with causes: Modern errors may include nested
causevalues, which can themselves contain circular structures. - ORM entities: Database models often include relation graphs with back references, making raw logging risky.
- Request and response objects: These usually contain internal references and native handles that should never be dumped wholesale.
- Class instances with methods: Even when there is no circular reference, methods and symbols can produce noisy or incomplete serialized output.
- BigInt values:
JSON.stringify()throws onBigIntunless you convert it first. - React internals: Logging framework-managed objects can expose private structures that are unstable across versions.
- Production logging providers: Some log aggregators perform their own serialization and can fail even if local development logging seems fine.
A more defensive serializer can handle BigInt too:
export function safeSerialize(value) {
const seen = new WeakSet();
return JSON.stringify(value, (key, currentValue) => {
if (typeof currentValue === 'bigint') {
return currentValue.toString();
}
if (typeof currentValue === 'object' && currentValue !== null) {
if (seen.has(currentValue)) {
return '[Circular]';
}
seen.add(currentValue);
}
return currentValue;
}, 2);
}
Best Practices for Server Component Debugging
- Log plain objects, not live framework objects.
- Prefer small, explicit debug payloads over entire instances.
- Convert complex values to strings before logging when debugging server rendering.
- Keep debug helpers in a shared utility so all server logs follow the same safe path.
- Remove or reduce noisy logging in production, especially around render paths.
FAQ
Why does console.log() fail here if Node normally supports circular objects?
Because in this case the log output is likely being processed by Next.js tooling or the React Server Components pipeline, not just printed directly by Node. Somewhere in that chain, the value is handled in a way that is not cycle-safe.
Is this a bug in my class implementation?
Usually no. A circular class graph is valid JavaScript. The problem is that framework-side logging or serialization is treating that graph as if it were plain JSON-safe data.
What is the safest workaround right now?
The safest workaround is to log a string or a sanitized plain object instead of the original instance. In practice, util.inspect(value) or a custom safeSerialize() helper is the most reliable fix.
Final Takeaway
When a Server Component crashes on console.log(), assume the issue is not the console itself but the serialization boundary around the log pipeline. Avoid logging raw cyclic objects, convert them to a safe string or summary object first, and centralize that behavior in a reusable helper. That approach fixes the immediate error and makes Server Component debugging much more predictable.