An unhandled exception error happens when a thrown exception reaches the top of the call stack with no handler, and the runtime decides what happens next. Depending on the language, that means a crashed process, a silently dead worker thread, or a broken UI that keeps running.
This guide shows how Node.js, Python, Java, .NET, and browsers each react, the global hooks that log failures before they disappear, and how to detect unhandled exceptions in production before a user reports them.
Key takeaways
- An unhandled exception error occurs when no handler in the call stack catches a thrown exception, so the language runtime takes over.
- Node.js exits the process by default for uncaught exceptions and, since Node 15, for unhandled promise rejections.
- Java kills only the thread that threw, so the JVM can keep running with a dead worker and no alert.
- OpenTelemetry defines stable exception attributes (exception.type, exception.message, exception.stacktrace) and now recommends recording exceptions as logs rather than span events.
- Multiline stack traces break log collectors that expect one event per line, which fragments unhandled exceptions into unreadable noise.
- Frontend unhandled exceptions need separate capture from backend exceptions, plus correlation to see the full picture.
- Global handlers (uncaughtException, sys.excepthook, UncaughtExceptionHandler) are for logging and graceful shutdown, not for recovery.
The real trade-off is speed versus safety. Catching everything defensively hides bugs, while catching nothing crashes processes. Most production incidents involving unhandled exceptions come from a runtime behavior the team didn’t know about, not from a try/catch someone forgot to write.
What is an unhandled exception error?
An unhandled exception error is a runtime fault that no try/catch block, error boundary, or handler intercepts before it reaches the top of the call stack. The language runtime then takes over. It usually prints a stack trace and terminates the process or thread. The exact behavior depends on the runtime.
What “unhandled” actually means
Every thrown exception travels up the call stack looking for a handler. If it reaches the top with nothing to catch it, the runtime treats it as unhandled and takes its own corrective action. That action varies sharply between languages, so understand it before you instrument anything.
A multiline stack trace parsing problem often makes an unhandled exception look worse than it is in your logs. A 20-line Java stack trace ingested without multiline handling becomes 20 disconnected log lines, each missing the context that made the original error meaningful. Fixing detection often starts with fixing how your log pipeline groups these lines back together.
How runtimes handle an uncaught exception
| Runtime | Default behavior | Global hook | Common trap |
|---|---|---|---|
| Node.js | Process exits (sync errors, and unhandled rejections since Node 15) | process.on(‘uncaughtException’), process.on(‘unhandledRejection’) | A logging-only unhandledRejection listener disables the default crash |
| Python | Main thread: traceback printed, interpreter exits with code 1. Other threads: that thread ends, the interpreter keeps running | sys.excepthook, threading.excepthook (3.8+) | asyncio task exceptions only surface as “Task exception was never retrieved” |
| Java | The throwing thread dies; the JVM exits only when no non-daemon threads remain | Thread.setDefaultUncaughtExceptionHandler | ExecutorService.submit() stores the exception in the Future, so the handler never fires |
| .NET | Process terminates | AppDomain.CurrentDomain.UnhandledException | The handler can log but cannot prevent termination |
| Browser JavaScript | Error logged to console; the page keeps running, often in a broken state | window error and unhandledrejection events | Minified stack traces are unreadable without source maps |
Node.js
In Node.js, an uncaught exception crashes the entire process by default, with no partial failure mode. Unhandled promise rejections used to print only a warning. Since Node 15, the default mode is throw, so they terminate the process too.
One detail catches many teams. Attaching an unhandledRejection listener replaces that default crash. A listener that only logs lets the process keep running in an unknown state. Rethrow instead, so the rejection flows into your uncaughtException handler:
process.on('uncaughtException', (err, origin) => {
logger.error('Uncaught exception', { err, origin });
// App state may be inconsistent: flush logs and telemetry, then exit
// and let your process manager restart the service.
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
// Attaching this listener disables Node's default crash.
// Rethrow so the uncaughtException handler logs it and exits.
throw reason;
});Python
An uncaught exception in Python’s main thread prints a traceback through sys.excepthook and exits the interpreter with status 1. An uncaught exception in any other thread goes to threading.excepthook (Python 3.8+). That thread ends, but the interpreter keeps running. Hook both:
import sys
import threading
import logging
def log_uncaught(exc_type, exc_value, exc_tb):
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_tb)
return
logging.critical("Uncaught exception", exc_info=(exc_type, exc_value, exc_tb))
def log_thread_exception(args):
name = args.thread.name if args.thread else "unknown"
logging.critical("Uncaught exception in thread %s", name,
exc_info=(args.exc_type, args.exc_value, args.exc_traceback))
sys.excepthook = log_uncaught
threading.excepthook = log_thread_exceptionFor asyncio services, an exception in a task nobody awaits never reaches either hook. It shows up only as a “Task exception was never retrieved” log line when the task is garbage collected.
If you run Python services, the Python APM setup guide shows how zero-code instrumentation attaches to Flask, Django, and FastAPI without writing a custom hook for every service.
Java
Java is the runtime most likely to surprise teams. An uncaught exception on a worker thread kills only that thread. The JVM process keeps running, silently, with one less worker doing its job.
Thread.setDefaultUncaughtExceptionHandler((thread, ex) ->
logger.error("Thread {} died from uncaught exception", thread.getName(), ex));That handler has a blind spot. Tasks passed to ExecutorService.submit() have their exceptions captured inside the returned Future. The exception only surfaces when someone calls future.get(), and the handler never fires. Tasks passed to execute() do reach the handler.
pool.submit(task); // exception stored in the Future; handler never fires
pool.execute(task); // exception reaches the UncaughtExceptionHandlerThis silent-thread-death behavior is why a Kafka consumer or thread-pool worker can stop processing messages for hours with no crash, no restart, and no alert, until someone notices the queue backing up.
.NET
In .NET, an unhandled exception on any thread terminates the process. AppDomain.CurrentDomain.UnhandledException lets you log it, but it cannot stop the shutdown:
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
{
logger.LogCritical(e.ExceptionObject as Exception,
"Unhandled exception. Terminating: {IsTerminating}", e.IsTerminating);
};How Middleware tracks unhandled exceptions
Capturing unhandled exceptions well means covering the backend, the frontend, and the link between them. Here is how Middleware handles each layer.
| Capability | How Middleware handles it |
|---|---|
| Backend coverage | OpenTelemetry-native APM for Java, Python, Node.js, Go, .NET, PHP, and Ruby, capturing stack traces and error types automatically |
| Frontend/RUM coverage | Native RUM with error tracking, session replay, and source map support to unminify JavaScript stack traces |
| Root-cause correlation | OpsAI correlates frontend errors with backend traces and logs, and can open a pull request with a proposed fix |
| Pricing model | Free Forever plan includes up to 100GB of data and 1K RUM sessions per month. Beyond that, pay-as-you-go pricing is $0.30/GB for metrics, logs, and traces and $1 per 1K RUM sessions. OpsAI error detection is free; root cause analysis and fixes are billed by tokens used |
Because backend and frontend errors land in one platform, a browser crash and the API request behind it can be investigated together instead of across separate tools.
5 best practices for handling unhandled exception errors
1. Attach global handlers without masking bugs
A global handler’s job is logging and graceful shutdown, not recovery. If you catch an uncaught exception and keep running as if nothing happened, you risk continuing in a corrupted state. Examples include a half-completed database transaction, a stale in-memory cache, or a connection pool with orphaned connections.
Node’s documentation recommends treating uncaughtException as a last-resort logger before a controlled restart. The same logic applies to Python’s sys.excepthook, Java’s UncaughtExceptionHandler, and .NET’s UnhandledException event.
Takeaway: Global handlers exist to log the failure and shut down cleanly. Using them to keep running past a corrupted state usually turns one incident into a harder second one.
2. Structure exception data with OpenTelemetry
Instead of parsing free-text log messages for the error type, use the OpenTelemetry exception semantic conventions. They define stable attributes: exception.type, exception.message, and exception.stacktrace. The spec recommends recording the exception’s dynamic type over its static type where the language supports it. This matters when a wrapped or rethrown exception would otherwise report the wrong class name.
OpenTelemetry has deprecated recording exceptions as span events in favor of recording them as log records. The exception.escaped attribute is also deprecated. Instrumentations migrate through the OTEL_SEMCONV_EXCEPTION_SIGNAL_OPT_IN environment variable, and the default still emits span events during the transition, so existing dashboards keep working. A log-based exception record looks like this:
{
"severity_text": "ERROR",
"attributes": {
"exception.type": "ConnectionError",
"exception.message": "Could not reach payment-service after 3 retries",
"exception.stacktrace": "Traceback (most recent call last): ..."
}
}For how OpenTelemetry structures log data, see our guide to OpenTelemetry logs.
Takeaway: Structured exception attributes turn “grep the logs for the error” into a queryable field. That is the difference between a five-second lookup and a ten-minute one during an incident.
3. Make multiline stack traces parseable
A stack trace is one logical event spread across many physical lines. Most log collectors default to treating each line as its own event. Without multiline handling, a four-line Java NullPointerException becomes four disconnected fragments, none of which is useful on its own.
The fix is a collector-side multiline rule: a start pattern, usually a timestamp regex, that tells the collector which lines belong to the previous event before shipping to storage. Our guide to log parsing covers this preprocessing step. Get it right before an incident, not during one.
Takeaway: If your log tool can’t show a complete, correctly grouped stack trace on the first click, the multiline parsing configuration is usually the real bug, not the application code.
4. Capture frontend unhandled exceptions separately
Browser listeners catch a different category of failure than backend handlers. Examples include a JS bundle regression that only triggers on iOS Safari, a third-party script throwing inside a promise chain, or a React component crashing mid-render. These never touch your backend logs.
window.addEventListener('error', (event) => report(event.error));
window.addEventListener('unhandledrejection', (event) => report(event.reason));React error boundaries catch errors during rendering, but not errors in event handlers, asynchronous code, or server-side rendering. You need the window listeners as well.
Source maps matter here. Without them, a minified production error points to bundle.min.js:1:48213 instead of the actual component and line. Upload source maps at build time so production stack traces are readable.
Takeaway: Frontend and backend unhandled exceptions are two separate capture problems. Skipping frontend instrumentation means an entire class of user-facing failures never reaches your dashboards. RUM error tracking closes that gap.
5. Alert on error rate, not error count
A raw count of unhandled exceptions is misleading during a traffic spike. Fifty errors out of 100,000 requests is very different from 50 errors out of 500. Alert on error rate as a percentage of throughput, with a threshold tuned to your normal baseline. That catches real regressions without paging someone over background noise. See logging vs monitoring for how to tie alerts to SLOs.
Pair that with an error budget from SRE practice. It gives teams a shared number for how many unhandled exceptions per week is acceptable, instead of treating every one as a fire drill.
Takeaway: Rate-based alerting with an agreed error budget turns exception monitoring from constant noise into a signal worth waking up for.
Deciding what needs a handler and what needs a fix
Before writing more try/catch blocks, ask three questions:
- Is this exception expected under normal operation, like a network timeout, or is it a genuine bug, like a null reference from bad state?
- Does recovering here leave the system safe enough to continue?
- Would a global handler actually help, or just delay the crash by one function call?
Four situations show how the answer changes with context:
- A Node.js checkout API where a payment webhook triggers an unhandled promise rejection mid-transaction. The fix isn’t a broader try/catch. Every
awaitin the payment path needs explicit error handling, so a failed charge can’t leave an order marked “paid” when it wasn’t. - A Python nightly ETL job that throws an uncaught exception on row 40,000 of 100,000, leaving a partially written table. Wrapping the whole job in one broad try/except hides which row failed. Wrapping each batch write in its own transaction with rollback prevents partial writes.
- A React single-page app that throws during a component render, but only on an older mobile Safari version. A top-level error boundary stops the blank-screen crash. Without source maps and session replay, though, no one can reproduce a bug that only one browser version triggers.
- A Java Kafka consumer thread that dies from an uncaught exception while the JVM stays alive and “healthy.” Process-level health checks won’t catch this. If the consumer runs through
ExecutorService.submit(), even anUncaughtExceptionHandlerwon’t fire. Consumer lag alerting is what reveals that messages have stopped flowing.
Where observability tooling closes the gap
Correct exception handling solves half the problem. Knowing that an unhandled exception happened in production, with full context, solves the other half. That is the gap between “we fixed the bug once we found it” and “we found it three days later from a customer complaint.” It is also where application performance monitoring earns its place.
Middleware’s APM auto-detects application errors across supported languages. It captures stack traces, error types, and code-level context without each service hand-rolling its own handler and shipping logic. Middleware real user monitoring connects backend traces and logs to frontend sessions. A frontend unhandled exception and the backend request that triggered it show up as one correlated incident, not two alerts in two tools.
OpsAI takes the next step. It pulls stack traces, logs, and error metadata together with your source code through a secure GitHub connection, runs root cause analysis, and can open a pull request with a proposed fix for human review. See how it maps errors to the responsible commit in error tracking with GitHub commit ownership.
Corgi Insurance uses Middleware to monitor its infrastructure and application stack. CEO Nico Laqua has said Middleware reduced the time his team spends debugging and resolving issues by nearly 90%.
FAQs
What causes an unhandled exception error?
An unhandled exception error is caused by a thrown exception that no code path catches. Common triggers include null references, failed network calls without error handling, promises with no rejection handler, and exceptions thrown inside background threads or thread pools.
What’s the difference between a handled and an unhandled exception?
A handled exception is caught by a try/catch or equivalent somewhere in the call stack and dealt with in code. An unhandled exception reaches the top of the call stack with no handler, so the runtime takes over. The runtime usually terminates the process or thread and prints a stack trace.
Does an unhandled exception always crash the whole application?
No. Node.js and .NET terminate the process by default. Python exits when the main thread fails but keeps running when a background thread fails. Java kills only the thread that threw, which is why silent thread death in Java is often harder to detect than a full crash.
Should I wrap my entire application in a global try/catch?
No. A single catch-all hides where a real bug lives and risks continuing after application state is already corrupted. Use targeted try/catch where you can meaningfully recover. Reserve global handlers for logging and graceful shutdown.
Does OpenTelemetry capture unhandled exceptions automatically?
Partly. Auto-instrumentation records exceptions that escape instrumented operations, such as an HTTP request handler. Exceptions outside any instrumented operation, like startup failures or crashes in background threads, still need a global hook to be logged.
Why do unhandled exceptions look garbled in my log tool?
Stack traces span multiple lines, but many log collectors treat each line as a separate event. Without multiline parsing configured, a single exception gets split into several disconnected log lines.
How do I catch unhandled exceptions in a React frontend?
Use a top-level error boundary for render-time errors. Add window listeners for the error and unhandledrejection events to catch failures in event handlers and async code, which error boundaries miss. Upload source maps at build time so production stack traces are readable.
What does “An unhandled exception has occurred in your application” mean in Windows?
That dialog comes from a .NET Windows application whose code didn’t catch an exception. If you are a user of the app, update or reinstall it and report the error details to the vendor. If you are its developer, the details show the stack trace you need to find the failing code path.

