ASVS 5.0 V16.5.4 requires a last resort error handler that catches all unhandled exceptions. It exists for two reasons named in the standard: so error details are not lost before they reach the logs, and so one error does not take down the whole process. It is a Level 3 requirement, so it applies to the most security critical applications.
Local try and catch blocks handle what you expected. The last resort handler handles what you did not.
Why it matters
An unhandled exception on a worker thread can end the process. A crash is a denial of service, and an attacker who finds a request that triggers one can repeat it. The crash also tends to lose the very details you need, because the runtime prints to a stream nobody collects.
There is a nuance. Some failures leave memory or state corrupted, and continuing is unsafe. The handler should log, respond or reject safely, and then either keep a healthy process running or exit cleanly and let the supervisor restart it.
How to implement it
Register a handler at every entry point: HTTP request pipeline, background workers, message consumers, scheduled jobs, and the process level hooks.
Node:
process.on("uncaughtException", (err, origin) => {
logger.fatal({ err: err.stack, origin }, "uncaught exception");
// state may be inconsistent: flush logs, stop accepting work, exit for the supervisor to restart
server.close(() => process.exit(1));
setTimeout(() => process.exit(1), 5000).unref();
});
process.on("unhandledRejection", (reason) => {
logger.error({ reason: String(reason) }, "unhandled rejection");
});
Python worker thread:
import sys, threading, logging
log = logging.getLogger("app")
def hook(args):
log.critical("thread died", exc_info=(args.exc_type, args.exc_value, args.exc_traceback))
threading.excepthook = hook
sys.excepthook = lambda t, v, tb: log.critical("unhandled", exc_info=(t, v, tb))
Java:
Thread.setDefaultUncaughtExceptionHandler((t, e) ->
LoggerFactory.getLogger("app").error("uncaught in thread {}", t.getName(), e));
For web frameworks, add a global handler that returns a generic response (see V16.5.1). Pair the process with a supervisor, such as systemd or a Kubernetes restart policy, with a backoff so a crash loop does not hammer dependencies.
How to test it
Add a test only endpoint or a hidden test hook that throws from a request handler, from a spawned thread, and from an async callback with no catch. Then verify:
- A log record with the full stack and a correlation id exists.
- The client received a generic response, or the connection closed cleanly.
- Other in flight requests finished, or the process restarted within your target time.
- Health checks reflect the recovery.
Run it in CI against a built image, not only unit tests.
Where the handler must sit
Every runtime has several places where an exception can escape. In a web server it is the request pipeline, plus the framework's own startup and shutdown hooks. In a worker it is the job loop and every thread or task spawned from it. In an async runtime it is promises, futures and goroutines that nobody awaits. Map each of these in your application and attach a handler to each. Missing one is how a single rogue task ends a process.
What the handler should do
Keep the handler small and defensive, because it runs when things are already wrong. It should write the log record first and synchronously if possible, include the stack, correlation id and a snapshot of safe context, increment an error counter, and then decide: recover, reject the current unit of work, or exit. Do not allocate heavily, call the network, or take locks that the failing code may hold.
If the handler itself can fail, give it a fallback, such as writing to standard error, which your platform collects. An error handler that throws is the worst case, since it hides the original problem.
Availability and restart policy
Process level resilience is the second half of the requirement. Run more than one instance behind a load balancer so a restart does not mean an outage. Set liveness and readiness probes that fail when the process is wedged, and cap restarts with backoff. Track crash counts as a metric, and page when they rise, since a crash loop is both an availability and a possible security signal.
Security angle
Attackers search for inputs that crash services. A last resort handler converts those inputs from an outage into a logged event. Review those logs regularly, group by stack, and fix the root causes so the handler stays a safety net, not a main path. Make sure the handler never echoes exception text to a caller, in line with the generic error requirement.
Common mistakes
- Handler registered in the web layer only, leaving workers uncovered.
- Swallowing the error and continuing in a state that is known to be bad.
- Logging to a buffer that is lost on exit.
- Returning the exception text to the client.
Testing in a realistic environment
Run the failure drills against the container image you ship, with the same supervisor and resource limits as production. A handler that works on a laptop can behave differently when the container has a read only filesystem, a tight memory limit, or a log driver that blocks. Include a drill where the log destination is unavailable, and confirm the handler still completes and the process exits or recovers as designed.
Document the expected behavior in a runbook, so on call engineers know a restart after an uncaught exception is normal, and what a crash loop means. Link the runbook from the alert that fires on crash counts.
Quick check
Should the process always keep running after the handler fires?
Not always. If state may be corrupted, finish logging, drain work and exit so the supervisor starts a clean process.