How to Fix: Stacktraces are gone and dev experience suffers
Missing stack traces turn simple runtime errors into blind debugging, and that is exactly why this issue hurts developer experience so much.
When an application throws an error like Cannot read properties of null but the terminal, browser overlay, or server logs no longer show a meaningful stack trace, developers lose the fastest path to the failing file, line number, and call chain. This issue typically appears when the error pipeline is swallowing stack information during logging, serialization, transformation, or custom error handling.
Understanding the Root Cause
In modern JavaScript and TypeScript toolchains, stack traces can disappear for a few common reasons:
- Error objects are being serialized with
JSON.stringify(error). The default Error fields such asmessageandstackare often non-enumerable, so serialization drops the most useful debugging details. - Custom error wrappers create a new error but fail to preserve the original
stackorcause. - Logging layers print only
error.messageinstead of the full error object. - Dev overlay or runtime formatting sanitizes output and strips internals before showing the error.
- Source maps are missing or broken, so even when a stack exists, it points to bundled output instead of source files.
- Async boundaries and promise chains may obscure the original call path when errors are rethrown incorrectly.
From the issue title, the likely regression is not that errors stopped happening, but that the developer-facing error reporting layer stopped preserving or rendering the full stack. The result is a worse DX: you know something failed, but not where or why.
A healthy error pipeline should preserve these pieces of information:
- message
- stack
- cause if present
- Relevant framework context such as route, component, loader, or request metadata
- Source-mapped file and line references in development
Step-by-Step Solution
The fix is usually to stop flattening errors into plain objects too early and to ensure your logger or error overlay prints the real Error instance.
1. Reproduce with a real runtime error
Start from a minimal failure case so you can verify whether the stack is missing in the browser, terminal, or both.
function renderUser(user) {
return user.profile.name;
}
renderUser(null);
If your output only shows a message like Cannot read properties of null without file and line details, the stack is getting lost after the exception is thrown.
2. Log the actual Error object, not a derived string
A common bug is this:
try {
renderUser(null);
} catch (error) {
console.error(error.message);
}
This prints only the message. Instead, log the full object:
try {
renderUser(null);
} catch (error) {
console.error(error);
}
If your logger supports structured metadata, include the stack explicitly:
try {
renderUser(null);
} catch (error) {
console.error('Application error', {
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
cause: error instanceof Error ? error.cause : undefined
});
}
3. Avoid JSON.stringify on Error instances
This is one of the most common root causes of missing stack traces.
try {
renderUser(null);
} catch (error) {
console.log(JSON.stringify(error));
}
Instead, normalize the error first:
function serializeError(error) {
if (error instanceof Error) {
return {
name: error.name,
message: error.message,
stack: error.stack,
cause: error.cause
};
}
return {
message: String(error)
};
}
try {
renderUser(null);
} catch (error) {
console.log(serializeError(error));
}
4. Preserve the original stack when wrapping errors
If your code catches an error and throws a new one, the original stack can be lost unless you chain it properly.
try {
renderUser(null);
} catch (error) {
throw new Error('Failed to render user');
}
Better:
try {
renderUser(null);
} catch (error) {
throw new Error('Failed to render user', { cause: error });
}
And if your runtime does not support cause, at least log both errors before rethrowing:
try {
renderUser(null);
} catch (error) {
console.error('Original error:', error);
throw error;
}
5. Ensure development source maps are enabled
If the stack exists but points to generated bundle files, the issue may look like missing stack traces when it is actually unusable stack traces. Verify that source maps are enabled in development and that your bundler is not disabling them.
// Example pattern only; exact config depends on your toolchain
export default {
devtool: 'source-map'
};
Also verify that any framework-specific dev server setting for source maps has not been disabled during optimization work.
6. Review custom global error handlers
Applications often install top-level handlers that unintentionally strip useful details.
window.addEventListener('error', (event) => {
console.error(event.message);
});
window.addEventListener('unhandledrejection', (event) => {
console.error(event.reason?.message || event.reason);
});
Improve them like this:
window.addEventListener('error', (event) => {
console.error('Unhandled error:', event.error || event.message);
});
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
});
7. Fix server-side logging separately from client-side logging
In full-stack frameworks, client and server error reporting often use different pipelines. If browser traces look fine but terminal traces do not, inspect your server logger, middleware, or runtime adapter.
app.use((err, req, res, next) => {
console.error('Request failed', {
path: req.path,
method: req.method,
message: err.message,
stack: err.stack
});
res.status(500).send('Internal Server Error');
});
The important detail is that you preserve err.stack before converting the error into a user-safe response.
8. Verify the fix with a regression test
If this is a framework or platform bug, add a test that asserts stack visibility in development.
it('prints stack traces for runtime errors in dev', async () => {
const output = await runDevServerAndCaptureError();
expect(output).toContain('Cannot read properties of null');
expect(output).toMatch(/renderUser|profile|stack/i);
});
This ensures the issue does not come back after future logging or overlay refactors.
Common Edge Cases
- Thrown non-Error values: Code that does
throw 'something failed'orthrow { message: 'x' }will not produce a proper stack. Always throw an Error. - Minified development builds: If a dev build is incorrectly minified, stack frames become much harder to read even when present.
- Cross-runtime differences: Browser, Node.js, edge runtimes, and test runners format stacks differently. A fix for one environment may not fully fix another.
- React error boundaries or framework boundaries: Some boundaries catch rendering errors and display fallback UI while suppressing verbose traces unless explicitly configured for development.
- Promise rejection transformations: Libraries sometimes wrap async failures into custom objects, dropping the original error instance.
- Logging adapters: Tools like pino, winston, or custom transport layers may require explicit serializers to include
stack.
If the issue still reproduces after applying the steps above, inspect the exact layer where the error changes shape: throw site, catch site, logger, framework overlay, dev server, or terminal formatter.
FAQ
Why do I see the error message but not the stack trace?
Because some part of the error handling flow is printing only error.message, serializing the error incorrectly, or replacing the original Error object with a plain object or string.
Can source maps fix missing stack traces?
Source maps do not create stack traces, but they make existing ones useful by mapping bundled frames back to the original source files. If the stack is totally absent, the problem is elsewhere in the error pipeline.
What is the safest way to preserve debugging details without leaking them to users?
Log the full Error including stack on the server or in development tools, but return a sanitized message in the UI or HTTP response. Separate developer diagnostics from user-facing error output.
In short, the real fix is to treat errors as first-class objects all the way through the development pipeline. Preserve the Error instance, avoid lossy serialization, keep source maps enabled, and make sure your logger or dev overlay renders the full stack trace. That restores the fast feedback loop developers expect when debugging runtime failures.