Windows Thread Error Modes: Scoped Flags, Inheritance, and Restoration
Use SetThreadErrorMode safely with inherited Windows flags, checked restoration on the same worker, and precise boundaries for serious-error dialogs.
Windows error mode is a policy for how the system handles certain serious errors. It is not a general exception handler, a way to turn failed operations into success, or a complete crash-reporting switch. SetThreadErrorMode lets code change that policy for the calling thread, which is useful when one worker needs a narrowly scoped behavior change without rewriting process-wide policy.
The difficult part is ownership. A thread inherits an error mode from its process, a setter replaces the thread’s mode with the supplied flags, and a worker can outlive the task that changed it. Correct code saves the actual prior mode, adds only the flags it intends to use, restores on the same thread on every exit path, and treats a failed restoration as a worker-health problem.
Separate thread policy from process policy
Every process has an associated error mode. A thread inherits that mode from its process, and GetThreadErrorMode retrieves the current mode for the calling thread. SetThreadErrorMode changes only the calling thread’s mode. It does not change the process mode or the mode of other existing threads.
SetErrorMode is the different, process-wide API. Microsoft recommends that applications call it with SEM_FAILCRITICALERRORS at startup to prevent critical-error dialogs from hanging an application. For other changes, Microsoft advises favoring the thread-specific API because it is less disruptive. A reusable library should not silently call SetErrorMode: doing so changes behavior outside the library’s own call and can affect code running on unrelated threads.
Thread-local scope is not task-local scope. A thread-pool worker may run a request, return to the pool, and later execute work belonging to another subsystem. If the first task leaves its error mode changed, the second task inherits that residue because it is running on the same thread. This is analogous to leaked thread-local state, but the state affects system error handling rather than an application data slot.
The thread’s inherited value also matters for restoration. Do not assume a new or pooled worker starts with mode zero. If process startup or an earlier owner established flags, restoring to zero discards them. Read and preserve the exact mode that exists at the boundary where the scope begins.
Know the documented flag set
SetThreadErrorMode documents these values:
SEM_FAILCRITICALERRORS(0x0001) suppresses the critical-error-handler message box; the system sends the error to the calling thread instead.SEM_NOGPFAULTERRORBOX(0x0002) prevents the Windows Error Reporting dialog from being displayed for the relevant error path.SEM_NOOPENFILEERRORBOX(0x8000) changes the legacyOpenFilefunction’s behavior when it cannot find a file. Instead of showing a message box,OpenFilereturns the error to its caller; this mode also overridesOF_PROMPT.
These flags do not mean the same thing. In particular, SEM_NOOPENFILEERRORBOX is not a general instruction to suppress prompts from CreateFile, the shell, or arbitrary file libraries. It is documented specifically for OpenFile. Likewise, SEM_NOGPFAULTERRORBOX is described in terms of the WER dialog. Do not market it as disabling all crash reporting, dump collection, debugger notification, or application-specific telemetry. Error-mode policy and a product’s crash-diagnosis policy are separate concerns.
Zero means the system default, which displays all error dialog boxes. It is not a universal “clear only this flag” operation. The value passed to SetThreadErrorMode is the new mode, not a bit mask of changes to apply. Passing only SEM_NOGPFAULTERRORBOX can therefore discard another flag that was already enabled on that thread. The thread-specific API’s documented flag list also does not include every flag documented for process-wide SetErrorMode; do not copy a process-mode value into a thread-mode call without checking the target API’s contract.
Snapshot, add, and restore the complete mode
The setter returns a nonzero BOOL on success and zero on failure. On success, its optional lpOldMode output receives the thread’s previous mode; pass NULL only when no later restoration is needed. On failure, call GetLastError immediately to capture extended error information.
To add one temporary behavior while retaining flags already present, read the current mode, combine the desired flag, and save the mode returned by the setter. The example below scopes SEM_FAILCRITICALERRORS around one operation and attempts restoration before normal return or a C++ exception that reaches the wrapper. This is not crash or structured-exception recovery: process termination, fail-fast, abrupt thread exit, or an SEH fault outside the wrapper’s C++ exception model can bypass cleanup. The reporting function is application-defined and must not throw while it records a restoration failure.
#include <windows.h>
#include <exception>
#include <type_traits>
// Application hook: record the error and mark the worker unhealthy.
void ReportModeRestoreFailureNoThrow(DWORD error) noexcept;
template<class Work>
DWORD RunWithoutCriticalErrorDialog(Work&& work)
{
static_assert(std::is_void_v<std::invoke_result_t<Work&&>>,
"work must return void; report failure by throwing an exception");
const DWORD observedMode = GetThreadErrorMode();
DWORD previousMode = 0;
const DWORD requestedMode = observedMode | SEM_FAILCRITICALERRORS;
if (!SetThreadErrorMode(requestedMode, &previousMode)) {
return GetLastError();
}
std::exception_ptr workFailure;
try {
work();
} catch (...) {
workFailure = std::current_exception();
}
if (!SetThreadErrorMode(previousMode, nullptr)) {
const DWORD restoreError = GetLastError();
ReportModeRestoreFailureNoThrow(restoreError);
if (workFailure) {
std::rethrow_exception(workFailure); // Preserve the primary failure.
}
return restoreError;
}
if (workFailure) {
std::rethrow_exception(workFailure);
}
return ERROR_SUCCESS;
}
The mode value passed to the setter is built by OR-ing the requested flag with the mode observed at scope entry. The previousMode output is still the value to restore: it is the setter’s own snapshot of the mode it replaced. Keeping both values explicit makes the distinction between “new mode” and “old mode” reviewable. If a component permits re-entrant code on the same thread to alter error mode during setup, centralize that ownership and define how such a conflict is handled rather than letting independent helpers overwrite each other’s policy.
The callback may throw, so the example captures that exception, attempts restoration on the same thread, reports a restoration failure through a non-throwing path, and then rethrows the original exception. On a normal path it returns the restoration error instead of claiming success. A production wrapper can return the work result as well, but it should preserve these two independent outcomes: the operation’s result and whether the thread was returned to its prior mode.
If restoration fails, the thread’s current mode is no longer known to match its prior state. Do not silently return that worker to a pool as clean. Record the numeric Win32 error, alert the owner, and quarantine or retire the worker according to the pool’s lifecycle rules. A destructor-only guard that discards the result of SetThreadErrorMode hides precisely the failure that can contaminate later work. RAII can provide a fallback attempt during unwinding, but a checked explicit restoration path is needed before normal control returns to unrelated tasks.
Treat nesting and thread identity as part of the contract
Nested scopes should compose by saving the mode that exists when each scope begins. If an outer scope has already added SEM_FAILCRITICALERRORS, an inner scope that adds SEM_NOOPENFILEERRORBOX should restore the outer mode, not a process default. The outer scope can then restore the baseline that existed before it started. A scope that simply clears its own flag may accidentally remove a flag also required by an outer owner.
Restoration must execute on the same thread that installed the temporary mode. Calling SetThreadErrorMode on another worker changes that other worker; it cannot repair the first thread’s state. Do not hand an old-mode integer to a cleanup callback that may run on a different executor. Keep setup, protected work, and restore in one synchronous worker invocation, or use an explicit same-thread continuation protocol if work is asynchronous.
For overlapped or queued work, define the boundary carefully. Returning from a function while I/O, callbacks, or continuations can still execute does not necessarily mean the logical operation has finished. The scope must cover the period in which the thread’s error behavior is meant to change, while not extending into later unrelated work. If an operation migrates between threads, each participating thread has its own mode and must follow its own policy.
Test state transitions without manufacturing a crash
Test the mode state directly on Windows rather than triggering a real disk fault or access violation merely to see a dialog disappear. A focused native fixture can capture a worker’s baseline with GetThreadErrorMode, enter a scope that adds one flag, and assert that the getter returns the combined mode while the scope is active. After explicit restoration, assert exact equality with the captured baseline, including any flags that were present before the test.
Add a nested-scope case and verify that the inner scope returns to the outer mode before the outer scope returns to baseline. Exercise both ordinary return and a thrown callback; in the latter case, verify that the original exception remains observable after cleanup. A worker-pool test should run a second task on the same worker and confirm it sees the original baseline, not the first task’s temporary flags. Keep any forced setter-failure case isolated from production workers and inspect the immediate GetLastError value.
SetThreadErrorMode and GetThreadErrorMode are documented for Windows 7 and later clients and Windows Server 2008 R2 and later servers. Include the Windows SDK header through Windows.h; the API is provided by Kernel32. A binary that must also load on older Windows versions should not assume the export exists there: raise the minimum supported version or resolve the optional API at runtime with GetProcAddress and handle an unavailable function explicitly. This guidance describes the documented contract and test plan; the code sample has not been executed on the macOS authoring host.
Related:
- Windows Fiber Local Storage: Cleanup Callbacks, Slot Ownership, and Index Retirement
- Windows Vectored Exception Handlers: Observation, Ordering, and Safe Teardown
Sources: