Designing Windows Apps to Leave Logs and Dumps When They Crash
· Updated: · Go Komura · Windows Development, Exception Handling, Logging, WER, Crash Dump, Bug Investigation
Revision history (1 updates, last updated Sep 1, 2026)
A log of the changes made to this article. Where a pre-update version was archived, it stays readable at a permanent DOI link.
- Retranslated as a full translation of the Japanese original. The previous English version was an abridgement that carried only part of the source, so sections, tables, Mermaid diagrams, figure captions and FAQ entries were missing. All of them have been restored to match the Japanese original, and the technical claims are the same as in the Japanese version. Read the version before this update (DOI: 10.5281/zenodo.21614543)
- First published
Cite this article(DOI: 10.5281/zenodo.21614542)
This article is archived on Zenodo. Below are both the DOI that always resolves to the latest version and the DOI pinned to the version you are reading.
Go Komura (2026). Designing Windows Apps to Leave Logs and Dumps When They Crash. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21614542 https://comcomponent.com/en/blog/2026/03/19/000-windows-app-crash-logging-best-practices/
- DOI (latest version)
- 10.5281/zenodo.21614542
- DOI (this version)
- 10.5281/zenodo.22217175
The most painful thing in Windows app bug investigation is the state where you know it crashed, but nothing was left behind that says why.
The problem becomes especially heavy in projects like these:
- It only crashes in the customer’s environment
- It only crashes after long-duration operation
- WPF / WinForms / Windows services / resident apps with low reproducibility
- COM, P/Invoke, native DLLs, or vendor SDKs are involved
- You captured “only the exception message,” with no context of what came right before
To be honest up front, though: the crashing process alone cannot “guarantee” that a log gets written. Once you include stack corruption, memory corruption, fast fail, forced termination, and power loss, the in-process final log is fundamentally best effort.
flowchart TB
accTitle: The final in-process log is best effort
accDescr: A diagram showing that once stack corruption, memory corruption, fast fail, forced termination, and power loss are in scope, the crashing process alone cannot guarantee that a log is written, and the final in-process log is fundamentally best effort.
a1["Stack corruption / memory corruption"] --> a4["Final in-process log"]
a2["Fast fail / forced termination"] --> a4
a3["Power loss"] --> a4
a4 --> a5["Fundamentally best effort"]
a5 -.-> a6["Guaranteed capture is impossible"]
Figure 1: The crashing process alone cannot guarantee that the last log line is written.
What you should aim for in practice is a configuration that does not pin its hopes solely on the inside of the crashing process. That is, you think in three layers:
- The regular, chronological log
- The final crash marker at the moment of the crash
- Crash evidence left by the OS or a separate process
In this article, with Windows desktop apps, resident apps, Windows services, and device-integration tools in mind, we organize the best practices for not losing investigability even when the app dies from an exception caused by a programming error.
1. The Conclusions First
The conclusions, listed up front.
- The most important thing is to not bet “the last log line” on a single in-process handler.
- The safest setup in practice is the combination regular log + final crash marker + WER LocalDumps.
- For long-duration operation, device integration, plugins, or mixed native SDKs, adding a monitoring process (watchdog / launcher / service) makes things considerably stronger.
- The iron rule in a crash handler is to do nothing heavy. Compression, HTTP transmission, DI resolution, UI dialogs, and complex JSON generation are out.
- At crash time, write only a short record locally; compression, upload, and notification are deferred to the next startup or a separate process.
- Using WinForms’s
ThreadExceptionor WPF’sDispatcherUnhandledExceptionto keep the app superficially alive is dangerous when the cause is a programming error. - Whether .NET or native, for exceptions that suggest corrupted state, the safer baseline is “record and exit” rather than “recover.”
- If you collect dumps, you must archive the PDBs and the shipped binaries at the same time, or you will not be able to read them later.
In short, the best practice is: “Don’t try to do everything at the moment of the crash. Divide the roles among before the crash, at the crash, and after the crash.”
flowchart TB
accTitle: Dividing the roles before, during, and after the crash
accDescr: A diagram showing the best practice of not trying to do everything at the moment of the crash, but dividing the roles into regular logging before the crash, a short local write at the moment of the crash, and compression, upload, and notification after the crash.
b1["Before the crash: keep a regular log"] --> b2["At the crash: write only a short local record"]
b2 --> b3["After the crash: compress, upload, notify"]
b3 -.-> b4["Do it after the next startup or in a separate process"]
Figure 2: Rather than doing everything at the moment of the crash, divide the roles across three points in time: before, during, and after.
1.1 Terms Used in This Article
Here are the words used from here on without further explanation.
| Term | Expansion | Meaning |
|---|---|---|
| WER | Windows Error Reporting | The Windows mechanism that catches and records an application’s abnormal termination on the OS side. The setting that keeps dumps locally is LocalDumps |
| Dump / minidump | crash dump | A file holding the process’s memory contents at the moment of the crash. It lets you inspect threads, stacks, and modules afterward |
| PDB | Program Database | The symbol file generated at build time. Without it, opening a dump gives you no function names and no line numbers |
| in-process | inside the process | Doing the work inside the crashing process itself. The opposite is a separate process |
| best effort | no guarantee | The property of “kept if things go well, never guaranteed.” The in-process log at the moment of the crash is exactly this |
fast fail / __fastfail |
immediate failure termination | A mechanism that terminates immediately with the fewest possible steps and no cleanup, once the state is judged corrupted. __fastfail in native code, Environment.FailFast in .NET |
| watchdog | monitoring process | A separate process that watches the main process’s startup, exit, and liveness from outside. It is sometimes built as a launcher or a parent service |
| heartbeat | liveness signal | The periodic signal that tells the watchdog “still running” |
| UNC path | Universal Naming Convention | A network share path in the form \\server\share\.... Use it at crash time and a momentary outage or a credential lookup will keep you waiting |
| ACL | Access Control List | The setting for who can read from and write to a folder. A regular cause of dumps and logs coming up empty |
| SEH | Structured Exception Handling | The native Windows exception mechanism. SetUnhandledExceptionFilter sits on top of it |
| CRT | C Runtime | The standard library implementation for C / C++. It has its own termination paths, separate from SEH |
| session | session ID | The value that identifies which launch instance is being discussed. It is the key for cross-referencing logs, dumps, and watchdog records |
Knowledge map for this article
To keep the cause traceable even when a Windows app goes down on an exception caused by a programming mistake, an effective design does not rely on the log of the crashing process itself but splits the evidence across three layers: a normal time-series log, a final crash marker written at the moment of the crash, and a dump produced by WER LocalDumps. AppDomain.UnhandledException, the WinForms ThreadException, and the WPF DispatcherUnhandledException are dangerous when used to keep the process alive in appearance only, and it is safer to use them as the entry point for recording and then terminate through an immediate-exit API such as Environment.FailFast, but calling FailFast inside UnhandledException swaps out the cause that the dump records. In native C++, the CRT termination paths have to be covered in addition to SEH, and when 24-hour operation or equipment control is involved, adding a monitoring process makes even the exit code and the restart count detectable from outside.
flowchart LR
accTitle: Crash-time log and dump design for Windows apps
accDescr: Diagram showing that a design which does not expect the evidence to live only inside the crashing process is built from a three-layer division of roles across the normal log, a final crash marker, and WER LocalDumps, from choosing between exception handlers such as AppDomain.UnhandledException and FailFast, and from external detection by a monitoring process
crash_time_logging_design["Crash-time logging and evidence design"]
wer_localdumps["WER LocalDumps"]
application_log["Application Log (Time-Series)"]
fatal_crash_marker["Fatal Crash Marker File"]
seh["SEH (Structured Exception Handling)"]
crt_termination_handler["CRT/C++ runtime termination paths"]
watchdog_process["Watchdog Process"]
high_reliability_operation_requirement["Demanding 24/7 and Device-Control Requirements"]
post_restart_processing["Deferred Processing After Restart"]
session_id_correlation["Session ID Correlation"]
dotnet_unhandledexception["AppDomain.UnhandledException"]
winforms_threadexception["Application.ThreadException (WinForms)"]
unexpected_exception_continuation["Continuing After a Program-Bug Exception"]
wpf_dispatcherunhandledexception["Application.DispatcherUnhandledException (WPF)"]
unobserved_task_exception["TaskScheduler.UnobservedTaskException"]
environment_failfast["Environment.FailFast"]
windows_application_event_log["Windows Application Event Log"]
dump_folder_acl["Dump Folder ACL"]
crash_dump["Crash dump"]
pdb["PDB (Program Database)"]
windbg["WinDbg"]
minidumpwritedump["MiniDumpWriteDump"]
wer_file_registration["Log Attachment via WerRegisterFile"]
crash_time_logging_design -->|"requires"| application_log
crash_time_logging_design -->|"requires"| fatal_crash_marker
crash_time_logging_design -->|"requires"| wer_localdumps
crash_time_logging_design -.->|"requires"| seh
crash_time_logging_design -.->|"requires"| crt_termination_handler
watchdog_process -->|"recommended for"| high_reliability_operation_requirement
fatal_crash_marker -->|"should come before"| post_restart_processing
fatal_crash_marker -.->|"requires"| session_id_correlation
fatal_crash_marker -->|"recommended for"| dotnet_unhandledexception
winforms_threadexception -->|"not recommended for"| unexpected_exception_continuation
wpf_dispatcherunhandledexception -->|"not recommended for"| unexpected_exception_continuation
unobserved_task_exception -->|"not recommended for"| fatal_crash_marker
environment_failfast -->|"uses"| windows_application_event_log
fatal_crash_marker -->|"recommended for"| seh
fatal_crash_marker -->|"recommended for"| crt_termination_handler
wer_localdumps -.->|"requires"| dump_folder_acl
crash_dump -.->|"requires"| pdb
crash_dump -->|"verified by"| windbg
minidumpwritedump -->|"implements"| crash_dump
watchdog_process -.->|"uses"| minidumpwritedump
wer_file_registration -->|"uses"| application_log
wer_localdumps -->|"should come before"| wer_file_registration
wer_localdumps -->|"implements"| crash_dump
fatal_crash_marker -->|"recommended for"| winforms_threadexception
In the diagram a solid line marks a relation that always holds and a dashed line marks a conditional one (the conditions are given per relation on the detail page). The full list of relations (24 in total, with evidence and certainty) and the definitions of the main concepts are collected on the knowledge map detail page (in Japanese). Data: JSON-LD / Turtle
2. Why In-Process Alone Cannot Be Made “Reliable”
Leave this vague and the design wobbles.
2.1 The Crashing Thread’s Own Context May Be Broken
Unhandled-exception hooks and top-level exception filters can run in the context of the broken thread. At that point, it is entirely normal that:
- The stack is already unsafe
- Heap corruption makes further allocation unsafe
- Waiting deadlocks because of locks held when the exception was thrown
- Objects the logger itself depends on are already broken
So it is safer to view the final handler not as “a place where anything is possible” but as “a place where very little is possible.”
flowchart TB
accTitle: Very little is possible in the final handler
accDescr: A diagram showing that unhandled-exception hooks can run in the context of the broken thread, where the stack and heap are unsafe, where waiting on a lock can hang, and where the logger dependencies may already be broken, so it should be seen as a place where very little is possible.
c1["Runs in the broken thread context"] --> c2["Stack and heap are unsafe"]
c1 --> c3["Waiting on a lock can hang"]
c1 --> c4["Logger dependencies may be broken"]
c2 --> c5["A place where very little is possible"]
c3 --> c5
c4 --> c5
Figure 3: The final handler is not a place where anything is possible; it is a place full of constraints.
2.2 Fast Fail and Corrupted-State Exceptions Assume “Minimal In-Process Activity”
Under memory corruption or fatal conditions, do not count on normal exception handling.
In particular, the native __fastfail family and anomalies suggesting corrupted state are designed to “terminate immediately with as little overhead as possible.”
In other words, the natural mindset is: a final in-process log is a bonus if it gets written; the primary evidence lives on the OS / separate-process side.
2.3 .NET’s Unhandled-Exception Event Is Not a Place for “Heavy Recovery” Either
.NET’s AppDomain.UnhandledException is useful, but
what you may safely do there should be limited to a short record.
- It can be affected by locks held when the exception was thrown
- It cannot safely capture absolutely everything, corrupted-state exceptions included
- Forcing a continuation policy here makes it easy to keep a half-broken process alive
It is realistic to treat the unhandled-exception event as “the final notification,” not as “a safe recovery point.”
flowchart TB
accTitle: The unhandled-exception event is the final notification
accDescr: A diagram showing that AppDomain.UnhandledException should be limited to a short record, that it cannot safely capture corrupted-state exceptions, and that the unhandled-exception event is the final notification rather than a safe recovery point.
d1["Unhandled-exception event"] --> d2["Limit it to a short record"]
d2 --> d3["Use it as the final notification"]
d1 -.-> d4["Not a safe recovery point"]
d4 -.-> d5["Forcing continuation keeps a half-broken process alive"]
Figure 4: The unhandled-exception event is the entry point for recording, not the place for recovery.
3. Recommended Architecture - Separate Crash-Time from After-Restart
The cleanest way to organize this is to separate what happens at crash time from what happens after restart.
First, here is a single picture of which process is responsible for each of the three layers, and where each one lands.
flowchart TD
subgraph APP["Application process"]
L1["Regular log<br/>append-only timeline"]
L2["Final crash marker<br/>write one line, then exit"]
end
subgraph WIN["Windows side"]
WER["WER LocalDumps<br/>dump saved out of process"]
end
subgraph WD["watchdog process"]
EX["Record exit code and exit time<br/>decide on restart"]
end
subgraph NEXT["Next healthy process started"]
POST["Compress / upload / notify<br/>detect the previous abnormal exit"]
end
DISK[("Fixed local folder")]
L1 --> DISK
L2 --> DISK
WER --> DISK
EX --> DISK
DISK --> POST
L1 -. exception thrown .-> L2
L2 -. process exits .-> WER
L2 -. process exits .-> EX
Figure 5: The three layers of evidence at a glance. The crashing process writes only two of them itself; the primary evidence sits outside.
There are three points to take away.
- The crashing process writes only the two items inside the “Application process” box itself. And the final crash marker only goes as far as “write one line, then finish.”
- The primary evidence sits outside the process. The WER dump and the watchdog record survive even when the app is broken.
- The keys for cross-referencing are a shared session ID and PID. Without them, the three pieces of evidence look like three separate incidents.
| Phase | Goal | Where it runs | What it does |
|---|---|---|---|
| Normal operation | Preserve the timeline | Inside the app | Structured logging, heartbeat, boundary events |
| At crash time | Drop minimal evidence | Inside the app + OS | Final crash marker, WER dump |
| Just after exit | Detect unexpected exit | Separate process | Record exit code, decide on restart, notify |
| After next startup | Heavy post-processing | A fresh, healthy process | Compression, upload, user notification, old-log cleanup |
With this split, the design becomes considerably more stable.
3.1 Minimal Configuration
For smaller business tools or internal WPF / WinForms apps, this much is often enough to start.
- Regular log: a local append-only file
- Final crash marker: a dedicated short file
- Dump: WER LocalDumps
- At next startup: show “The application terminated abnormally last time. Diagnostic information is available.”
3.2 Stronger Configuration
It is worth going one level stronger under requirements like these.
- 24/7 operation
- Device control, monitoring, resident operation
- Lots of COM / P/Invoke / native SDKs
- Child processes, plugins, or script execution
- “Stuck and staying stuck” is unacceptable in the customer environment
In that case, splitting into:
- Worker process: the main workload
- Launcher / watchdog / service: startup supervision, exit recording, restart
- WER LocalDumps: on the worker side
- Next startup or the watchdog: diagnostic-information collection
makes the setup much more workable in real-world practice.
flowchart TB
accTitle: Division of work in the stronger configuration
accDescr: A diagram showing that for 24/7 operation or device control the main workload goes to a worker process while a launcher or watchdog handles startup supervision, exit recording, and restart, with WER LocalDumps configured on the worker side and the next startup or the watchdog collecting the diagnostic information.
e0["Stronger requirements (24/7, device control, and so on)"] --> e1["worker: the main workload"]
e0 --> e2["watchdog: startup supervision, exit recording, restart"]
e1 -.-> e3["Configure WER LocalDumps on the worker side"]
e2 -.-> e4["Next startup or the watchdog collects the diagnostics"]
Figure 6: In the stronger configuration, the main workload and the supervision live in separate processes with fixed roles.
4. Best Practices for the Regular Log
If you try to fight with only the last line at crash time, you usually lose. What really pays off is the regular log up to the moment before.
4.1 Logs Are “Information That Correlates Later,” Not “Prose for Humans”
The minimum items you want in the regular log:
- UTC timestamp
- Elapsed time since process start
- PID / TID
- App name, version, build number, commit identifier
- Session ID
- Operation ID / job ID / correlation ID
- Module name / screen name / worker name
- The most recent external effects
- File writes
- DB updates
- Device commands sent
- Communication requests
- Exception type, HRESULT / Win32 error / exception code
- A summary of the key input parameters
- Target IDs, to the extent they contain no secrets
Our recommendation is one event per line, in JSON Lines or key=value format.
With JSON Lines, a single event comes out at about this granularity.
{"ts":"2026-03-18T01:15:33.412Z","up_ms":184213,"pid":1234,"tid":9,"app":"MyApp","ver":"3.2.1.884","commit":"9f1c2ab","session":"4f1c","level":"Warning","module":"DeviceWorker","op":"JOB-20260318-0042","event":"ExternalCommandSent","target":"COM3","cmd":"SEQ_START","timeout_ms":3000}
It is long, but that one line alone tells you when, on which launch instance, which version, in the middle of which operation, did what. It connects to the dump side through pid and session, and to the build side through ver and commit.
In key=value format, the same content looks like this.
ts=2026-03-18T01:15:33.412Z up_ms=184213 pid=1234 tid=9 app=MyApp ver=3.2.1.884 commit=9f1c2ab session=4f1c level=Warning module=DeviceWorker op=JOB-20260318-0042 event=ExternalCommandSent target=COM3 cmd=SEQ_START timeout_ms=3000
Rather than leaving long prose for humans, what matters more is “being able to cross-reference three files later.”
flowchart TB
accTitle: One log line links three pieces of evidence
accDescr: A diagram showing that a single regular log line connects to the dump side through pid and session and to the build side through ver and commit, so that being able to cross-reference three files later matters more than long prose for humans.
f1["One line of the regular log"] --> f2["pid / session lead to the dump"]
f1 --> f3["ver / commit lead to the build"]
f2 --> f4["Three files can be cross-referenced"]
f3 --> f4
Figure 7: A single log line connects to the other evidence through pid, session, ver, and commit.
4.2 Write Critical Events Synchronously
Making every regular log write synchronous gets heavy. But entrusting everything to an asynchronous buffer means it all evaporates at the moment of the crash.
So in practice it is realistic to vary the handling by level.
- Fine-grained
Informationevents: buffering is fine Warningand above: flush early- Important boundary events: write synchronously
- ProcessStart
- ConfigLoaded
- WorkerStarted
- ExternalCommandSent
- TransactionCommitted
- RecoveryStarted
- FatalPathEntered
The point is: at least the business-level boundaries must actually make it to disk.
flowchart TB
accTitle: Choosing the write mode by level
accDescr: A diagram showing that fine-grained Information events may be buffered, that Warning and above should be flushed early, and that business boundary events should be written synchronously, varying the handling by level.
g0["Log writes"] --> g1["Information: buffering is fine"]
g0 --> g2["Warning and above: flush early"]
g0 --> g3["Boundary events: write synchronously"]
g3 -.-> g4["Business boundaries actually reach disk"]
Figure 8: Neither everything synchronous nor everything buffered - choose the write mode by level.
4.3 Separate “the Regular Log Being Written Now” from “the Final Crash Marker”
This matters a great deal.
If you try to put everything into a single rolling log:
- It was mid-rotation
- It was still sitting in the async queue
- The logger itself died right after the exception
- The log line was cut off mid-write
all of these happen.
So we recommend splitting into at least two files.
app-<session>.jsonlThe regular chronological logfatal-last.logorfatal-<session>.logDedicated to the final crash marker
Just having “where the last line goes” be unambiguous helps enormously in the field.
flowchart TB
accTitle: Separate the regular log from the fatal marker
accDescr: A diagram showing that putting everything into a single rolling log loses the last line to rotation, to the async queue, or to the death of the logger itself, so the chronological log and a dedicated final crash marker file should be kept as two separate files.
h1["Everything in one rolling log"] --> h2["Lost to rotation, a queue, or a dead logger"]
h2 -.->|"instead"| h3["Split into a regular log and a fatal marker"]
h3 --> h4["Where the last line goes becomes unambiguous"]
Figure 9: Fix the home of the last line in a file separate from the regular log.
4.4 Log Destination: Fixed Local Path, Never a Network Target
Relying on UNC paths, NAS, HTTP, or cloud APIs at crash time is dangerous, because of:
- Momentary network outages
- DNS delays
- Expired credentials
- Blocking on the UI thread
- Insufficient service-account permissions
At crash time, drop to a fixed local path first. Sending happens after the next startup or from a separate process.
4.5 Put the Session in the File Name
A date alone is not enough, because the app restarts multiple times on the same day.
For example:
Logs\
MyApp_20260318_101530_pid1234_session-4f1c.jsonl
MyApp_fatal_20260318_101533_pid1234_session-4f1c.log
MyApp_watchdog_20260318.jsonl
Just making “which launch instance is this about” unambiguous changes the speed of analysis considerably.
5. Best Practices for the Final Crash Marker
This is not the place to build a full-featured logger. It is the place to write once, briefly, leaning toward reliability.
5.1 The Goal Is “Pinning the Entry Point,” Not “Cause Details”
The information in the final crash marker is stronger when narrowed down.
- UTC of occurrence
- PID / TID
- Session ID
- Version / build number
- Which hook it came from
AppDomain.UnhandledExceptionApplication.ThreadExceptionDispatcherUnhandledExceptionSetUnhandledExceptionFilter_set_invalid_parameter_handlerset_terminate
- Exception type or exception code
- A short message, if possible
- The most recent operation ID
- The regular log’s file name
- The expected dump folder
That is enough.
flowchart TB
accTitle: The marker exists to pin the entry point
accDescr: A diagram showing that the final crash marker narrows down what it records in order to pin the investigation entry point, namely which hook and which exception plus the regular log file name and the expected dump folder, rather than to explain the cause in detail.
i1["Final crash marker"] --> i2["Which hook, exception type, session"]
i1 --> i3["Regular log name and dump folder"]
i2 --> i4["The investigation entry point is pinned"]
i3 --> i4
i4 -.-> i5["Leave the cause details to the dump and the regular log"]
Figure 10: The marker’s job is not to explain the cause but to pin the entry point for the investigation.
5.2 What Not to Do in a Crash Handler
Every one of these is a land mine with high probability.
- Resolving the logger from a DI container
- Using async / await
- Spawning Tasks
- Waiting on locks
- Assembling complex JSON
- Touching COM objects
- Showing UI dialogs
- Compressing
- HTTP / SMTP / Slack / Teams transmission
- Analyzing and summarizing the dump
- Swallowing the exception and continuing
A crash handler is not a continuation of the normal processing flow. Lean toward “do the minimal local write, then end.”
5.3 What to Do in a Crash Handler
Conversely, what to do is quite simple.
- Prevent reentry
- Write one line
- Flush
- Exit
In that order.
Ideally, use:
- A dedicated folder created in advance
- A path whose existence was verified in advance
- A destination whose ACLs were verified in advance
Over-flushing the regular log is heavy, but the fatal marker is extremely low-volume, so flushing hard is fine here alone.
In .NET, FileStream.Flush(true); in native code, FlushFileBuffers - design becomes easier when you treat it as “this one line goes to disk right now”.
flowchart TB
accTitle: The four steps of a crash handler
accDescr: A diagram showing that a crash handler does only four things, namely prevent reentry, write one line, flush, and exit, and that because the fatal marker is extremely low-volume it is fine to flush it hard all the way to disk.
j1["1. Prevent reentry"] --> j2["2. Write one line"]
j2 --> j3["3. Flush"]
j3 --> j4["4. Exit"]
j3 -.-> j5["This one line goes to disk right now"]
Figure 11: The handler does only these four steps, and the order does not change.
5.4 Do Not Try to Keep the App Alive
For unexpected exceptions originating from programming errors, it is safer to consider the final handler a recording device, not a recovery device.
The cases where “do not continue” should be the baseline:
- Even a
NullReferenceExceptionorInvalidOperationExceptionthat occurred mid-update of shared state - Unexpected exceptions on the UI thread
- Unexpected exceptions that leaked out of monitoring or parent loops
AccessViolationExceptionStackOverflowException- Anomalies at the native boundary
- CRT invalid parameter / purecall / terminate
The desire to “not let it crash” is understandable, but surviving half-broken is usually worse for both diagnosis and operations.
When terminating, consider immediate-termination APIs - Environment.FailFast in .NET, RaiseFailFastException or __fastfail in native code - and design without counting on finally blocks or normal cleanup.
flowchart TB
accTitle: A recording device, not a recovery device
accDescr: A diagram showing that for unexpected exceptions originating in programming errors the final handler should be treated as a recording device rather than a recovery device, because recording and terminating through an immediate-termination API is safer than surviving half-broken.
k1["Exception from a programming error"] --> k2["Survive half-broken"]
k1 --> k3["Record and exit"]
k2 -.-> k4["Worse for diagnosis and for operations"]
k3 --> k5["Consider FailFast and similar immediate-termination APIs"]
Figure 12: The final handler becomes a device for recording and exiting, not for recovering.
5.5 A Minimal Implementation in C#
Dropped straight into C# on .NET 6 or later, everything above comes out at about this size. No DI, no logger.
using System;
using System.IO;
using System.Text;
using System.Threading;
internal static class FatalMarker
{
// Use a fixed local path created in advance and already verified with a write test
private static readonly string LogDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MyApp", "Logs");
// The key for cross-referencing the regular log, the dump, and the watchdog record
private static readonly string SessionId = Guid.NewGuid().ToString("N").Substring(0, 8);
// Reentry guard. 0 means nothing has been written yet
private static int _written;
/// <summary>Call this exactly once, right after the app starts.</summary>
public static void Install()
{
// Create it here, because we do not want to call CreateDirectory at crash time
Directory.CreateDirectory(LogDirectory);
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
}
/// <summary>Call this wherever you decide the process must not continue.</summary>
public static void FailNow(string reason)
{
Write("FailNow", null, reason);
Environment.FailFast(reason);
}
private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
Write("AppDomain.UnhandledException", e.ExceptionObject as Exception, null);
// Do not terminate here. This is an unhandled exception, so the CLR
// proceeds to its default termination path afterward.
// Calling FailFast here would swap the cause recorded in the WER dump
// from the original exception to FailFast.
}
private static void Write(string hook, Exception ex, string note)
{
// Do nothing on the second and later calls
if (Interlocked.Exchange(ref _written, 1) != 0)
{
return;
}
try
{
string fileName = string.Format(
"MyApp_fatal_{0:yyyyMMdd_HHmmss}_pid{1}_session-{2}.log",
DateTime.UtcNow, Environment.ProcessId, SessionId);
string path = Path.Combine(LogDirectory, fileName);
// Build the line with string concatenation only, without calling a serializer
string line = string.Join("\t",
"ts=" + DateTime.UtcNow.ToString("O"),
"pid=" + Environment.ProcessId,
"tid=" + Environment.CurrentManagedThreadId,
"session=" + SessionId,
"hook=" + hook,
"type=" + (ex != null ? ex.GetType().FullName : "none"),
"message=" + Flatten(ex != null ? ex.Message : note),
"log=" + LogDirectory);
using (var stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read))
{
byte[] bytes = new UTF8Encoding(false).GetBytes(line + Environment.NewLine);
stream.Write(bytes, 0, bytes.Length);
// Passing true pushes it all the way to disk instead of the OS cache
stream.Flush(true);
}
}
catch
{
// If this fails, there is nothing left to do. Swallow it and finish
}
}
private static string Flatten(string s)
{
return string.IsNullOrEmpty(s) ? string.Empty : s.Replace("\r", " ").Replace("\n", " ");
}
}
On the calling side, all you do is call Install() right after startup. Forget this and the code above does not do a single byte of work.
internal static class Program
{
private static void Main(string[] args)
{
FatalMarker.Install();
// From here on, the normal app startup path
RunApplication(args);
}
private static void RunApplication(string[] args)
{
// Example: once shared state is judged corrupted, bring it down instead of continuing
// FatalMarker.FailNow("device state inconsistent");
}
}
All these 60 lines do is honor the four items listed in 5.3.
Interlocked.Exchangeprevents reentry- String concatenation alone writes one line
Flush(true)pushes it to disk- The
UnhandledExceptionpath does not continue
Environment.FailFast writes the message to the Windows Application event log, terminates the process immediately, and includes that content in the error report. In other words, FailFast itself adds one more piece of evidence. As noted above, though, calling it on the unhandled-exception path changes how the dump looks, so choose the call site carefully.
flowchart TB
accTitle: Choose where to call FailFast
accDescr: A diagram showing that Environment.FailFast writes to the application event log and terminates immediately, adding one more piece of evidence, but that calling it on the unhandled-exception path swaps the cause recorded in the WER dump from the original exception to FailFast, so the call site must be chosen carefully.
l1["Environment.FailFast"] --> l2["Writes to the event log, then exits immediately"]
l2 --> l3["One more piece of evidence"]
l1 -.-> l4["Called on the unhandled-exception path"]
l4 -.-> l5["The dump cause is swapped for FailFast"]
Figure 13: FailFast adds evidence, but calling it inside an unhandled exception swaps out the cause.
6. Framework-Specific Notes
6.1 .NET in General: AppDomain.CurrentDomain.UnhandledException
This is useful as the final notification. But avoid heavy recovery work here.
The basic usage is simple.
- Write the final crash marker
- If needed, leave a minimal message in the Windows Event Log
- Do not continue
- Do not wait or retry here
UnhandledException is convenient, but it is safer not to assume the app can be returned to a healthy state from here.
6.2 WinForms: Application.ThreadException
The tricky part here is that it catches unhandled UI-thread exceptions and lets the app continue, at least superficially.
Using it to turn expected business-input errors into dialogs is one thing, but it is not suited to continuing after unexpected exceptions caused by programming errors.
If root-cause investigation is your priority:
- Do only minimal recording in
ThreadException - Or lean toward
UnhandledExceptionMode.ThrowException - Then terminate the process, leaving the dump and the logs
That is safer.
6.3 WPF: Application.DispatcherUnhandledException
WPF is similar.
- It primarily targets exceptions on the UI thread
- Setting
Handled = trueallows superficial continuation - But do that against a programming error and the screen state and internal state drift apart easily
So in WPF too, it is safer to use it as an entry point for recording, not as a life-support device for continuation.
flowchart TB
accTitle: Do not use UI events as life support
accDescr: A diagram showing that WinForms ThreadException and WPF DispatcherUnhandledException allow superficial continuation, but that against programming errors the screen state and the internal state drift apart, so they are safer as an entry point for recording followed by termination that leaves a dump and logs.
m1["Unhandled exception on the UI thread"] --> m2["Superficial continuation is possible"]
m2 -.-> m3["Screen state and internal state drift apart"]
m2 --> m4["Use it as an entry point for recording"]
m4 --> m5["Terminate and leave the dump and the logs"]
Figure 14: The WinForms and WPF UI events belong at the entry point for recording, not in life support.
6.4 Do Not Make TaskScheduler.UnobservedTaskException a Primary Path
This is not “the last bastion just before the crash.”
It can help detect dropped Task exceptions,
but as a reliable recording path at crash time, it is weak.
So use it for:
- Catching unobserved exceptions early
- Flushing out
Taskdesign omissions during development
but do not make it your primary crash handler.
6.5 Native Win32 / C++: Do Not Over-Trust SetUnhandledExceptionFilter
On the native side, it is tempting to count on SetUnhandledExceptionFilter.
However, it runs in the context of the faulting thread, so it is affected by:
- An invalid stack
- Deep recursion
- An already-corrupted heap
- Locks held at exception time
Therefore, SetUnhandledExceptionFilter is best regarded as
a best-effort entry point for receiving the final notification.
6.6 In Native C++, Also Catch the CRT Termination Paths
In native C++, watching only unhandled SEH leaves gaps.
Specifically, keep an eye on:
_set_invalid_parameter_handler_set_purecall_handlerset_terminate
This family exists to catch “termination paths” originating in the C runtime and the C++ runtime.
In practice, the sound approach is:
- Write the final crash marker in these handlers too
- But do no heavy recovery work
- Terminate reliably
- Leave the primary evidence to WER / the dump
flowchart TB
accTitle: SEH alone leaves gaps
accDescr: A diagram showing that in native C++ watching only SetUnhandledExceptionFilter misses the termination paths originating in the CRT and the C++ runtime, so the invalid parameter, purecall, and terminate handlers should also write a minimal record while WER and the dump carry the primary evidence.
n1["Only SetUnhandledExceptionFilter"] --> n2["Misses the CRT termination paths"]
n2 --> n3["Also catch invalid parameter / purecall / terminate"]
n3 --> n4["Write a minimal record, then terminate reliably"]
n4 -.-> n5["Leave the primary evidence to WER / the dump"]
Figure 15: Native C++ stops leaking cases only once the CRT termination paths are covered alongside SEH.
7. Build on WER LocalDumps
This part is quite powerful in practice.
7.1 The First Recommendation Is WER LocalDumps
In the sense of “leaving minimal evidence, leaning reliable, after the crash,” WER LocalDumps is the easiest thing to start with.
The reasons are simple.
- The OS side can persist the dump
- Easy to roll out with no extra tools
- Configurable per application
- It moves the primary crash evidence outside the in-process world
What logs alone cannot tell you:
- Which thread crashed
- On which stack it crashed
- Which module boundary it was
- Whether managed / native / COM / SDK is the suspect
Being able to see this afterward is its strength.
flowchart TB
accTitle: Why WER LocalDumps is strong
accDescr: A diagram showing that WER LocalDumps persists dumps on the OS side, is configurable per application, moves the primary evidence outside the in-process world, and lets you see afterward which thread crashed on which stack and at which module boundary.
p1["WER LocalDumps"] --> p2["The OS side persists the dump"]
p1 --> p3["Configurable per application"]
p2 --> p4["Primary evidence moves outside the process"]
p3 --> p4
p4 -.-> p5["Thread, stack, and module boundary become visible"]
Figure 16: WER LocalDumps is the foundation that moves the primary evidence outside the crashing process.
7.2 Typical Configuration
For example, to keep dumps for MyApp.exe in C:\CrashDumps\MyApp:
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpFolder /t REG_EXPAND_SZ /d "C:\CrashDumps\MyApp" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpCount /t REG_DWORD /d 10 /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpType /t REG_DWORD /d 2 /f
Rough-and-ready settings like this are fine at first.
| Value | Initial recommendation |
|---|---|
DumpFolder |
A dedicated folder |
DumpCount |
5-10 |
DumpType |
2 on development machines; 1 or 2 in the field, depending on disk space and confidentiality requirements |
7.3 Always Verify the ACL of the Dump Destination
Same as with logs: configuring a folder you cannot write to is pointless.
Especially with:
- Windows services
- Privilege-separated child processes
- Restricted accounts on field machines
- UAC involvement
the destination ACL is the main reason no dump ever appears.
For the destination, verify all the way through:
- Pre-creation
- A write test
- Retention limits
- Whether operations staff can actually go look there
flowchart TB
accTitle: Preventing a dump destination that never receives anything
accDescr: A diagram showing that with Windows services and restricted accounts the classic failure is configuring a dump destination the process cannot write to, so the destination should be created in advance and verified with a write test, retention limits, and a check that operations staff can reach it.
q1["Services and restricted accounts"] --> q2["The destination folder is not writable"]
q2 --> q3["The dump never lands"]
q3 -.->|"to prevent it"| q4["Pre-create the folder and run a write test"]
q4 --> q5["Check retention and whether staff can reach it"]
Figure 17: The main cause of a missing dump is the destination ACL, and a write test in advance prevents it.
7.4 When You Want to Attach the Current Log to a WER Report
If you use WER reporting to Microsoft or your own WER pipeline, there is also the method of calling WerRegisterFile to register the current log file for inclusion in the error report.
However, treat this as an additional channel, not a replacement for local storage. What you truly want at crash time is, first, a reliable-leaning record on the local machine.
The practical order is:
- Local regular log
- Local fatal marker
- Local dump
- If needed, also register related files in the WER submission path
7.5 Keep the Build/Version Records, Not Just Dumps
Even with a dump in hand, if later:
- The EXE / DLLs from that build are gone
- The PDBs are gone
- Nobody knows which commit the build came from
then you are in a weak position.
At minimum, keep:
- The shipped binaries
- The corresponding PDBs
- The version
- The build timestamp
- The commit identifier
- The installer version
Dump collection and PDB archiving are a set.
flowchart TB
accTitle: Dump collection and PDB archiving are a set
accDescr: A diagram showing that a dump becomes unreadable later if the EXE and DLLs from that build, the PDBs, or the commit the build came from were not kept, so archiving the shipped binaries, the PDBs, the version, and the commit identifier goes together with dump collection.
r1["Only the dump is kept"] --> r2["No PDBs or binaries from that build"]
r2 --> r3["Unreadable later"]
r3 -.->|"so"| r4["Archive the shipped binaries and the PDBs"]
r4 --> r5["Keep the version and commit identifier too"]
Figure 18: A dump becomes readable only when the PDBs and binaries from the same build are still around.
7.6 Getting as Far as Opening the Dump You Captured
“Dumps are piling up, but nobody has ever opened one” really does happen. Deeper work belongs in a dedicated article, so here are just the first ten minutes.
Two things to prepare.
- Gather the PDBs and shipped binaries from the same build as that dump into a single folder
- Get hold of WinDbg, part of the Debugging Tools for Windows in the Windows SDK
After that, open the .dmp in WinDbg and type these in order.
.sympath srv*C:\symbols*https://msdl.microsoft.com/download/symbols;C:\myapp\pdb
.reload /f
.ecxr
!analyze -v
~*k
lm v
Here is what each one does.
| Command | What it does |
|---|---|
.sympath |
Sets where symbols are searched for. Specify both the Microsoft symbol server and your own PDB store |
.reload /f |
Forces symbols to be reloaded |
.ecxr |
Switches to the register context at the time of the exception. Forget this and you end up looking at the WER wait stack instead of the crash site |
!analyze -v |
Automatically analyzes the current exception and prints the details. Read this first |
~*k |
Prints the call stacks of all threads. Shows what the threads other than the crashing one were doing |
lm v |
Prints the loaded modules and their versions. Used to match the shipped artifacts against the build number |
If you get this far and “no function names” or “no line numbers” appear, the cause is usually PDBs from a different build. Go back to the build/version records in 7.5.
flowchart TB
accTitle: The first ten minutes with a dump
accDescr: A diagram showing the opening flow of gathering the PDBs and shipped binaries from the same build, opening the dump in WinDbg, moving to the context at the time of the exception before reading the analysis, and going back to the build records when function names do not appear because the PDBs come from a different build.
s1["Gather PDBs and binaries from the same build"] --> s2["Open the dump in WinDbg"]
s2 --> s3["Move to the exception context and analyze"]
s3 -.-> s4["Forget .ecxr and you read the WER wait stack"]
s3 --> s5{"Do function names and line numbers appear"}
s5 -->|"no"| s6["PDBs from a different build - go back to the build records"]
Figure 19: Dump analysis starts in this order: prepare, open, move to the exception context.
The deeper story of collection and analysis is covered in the related articles at the end.
8. How to Think About MiniDumpWriteDump and Custom Crash Reporters
There are situations where a custom implementation is needed.
- You want a “Save diagnostic information” button in the UI
- You want to bundle logs and configuration files too
- You want to handle a group of child processes together
- You want custom masking before automatic upload
The most important thing here, though, is to not pile even the dump-taking work onto the crashing side.
8.1 A Separate Process Beats Self-Dump
MiniDumpWriteDump is powerful, but
calling it from a separate process is safer than calling it from inside the crashed process itself.
A typical configuration looks like this.
- The worker detects the anomaly
- If possible, it notifies a helper via an event or a named pipe
- The helper takes a dump of the worker
- The helper bundles the
tailof the logs and the configuration files - The helper places everything in an upload queue after exit
This way, even if the worker is broken, the helper side is still healthy.
sequenceDiagram
accTitle: Taking a dump from a separate helper process
accDescr: A diagram showing the flow where the worker detects an anomaly and notifies a helper through an event or a named pipe, the healthy helper takes a dump of the worker, bundles the logs and configuration files, and places them in an upload queue after exit.
participant W as worker process
participant H as helper
W->>H: Notify the anomaly via an event or a pipe
H->>W: Take a dump of the worker
H->>H: Bundle the tail of the logs and the config files
H->>H: Place them in the upload queue after exit
Note over H: The helper stays healthy even when the worker is broken
Figure 20: Leave dump capture to a healthy helper process rather than to the crashing side.
8.2 If It Must Be In-Process, Push It onto a Dedicated Thread
Even when a separate process is not an option, keeping a dedicated thread reserved for dumping is at least better.
Still, the essence remains best effort. “We added a custom dump implementation, so we’re 100% safe” does not follow.
8.3 Defer Heavy Work to the Next Startup
Things people tend to want in a custom reporter:
- Zip compression
- Matching against symbol information
- Server upload
- Screen capture
- Fetching extra information from the DB
All of these go after restart or to the helper side, not to crash time.
9. What Changes When You Add a Monitoring Process
For long-duration operation, a monitoring process pays off heavily.
9.1 What the Monitoring Process Records
The watchdog / launcher / parent service can record:
- Child process start time
- Launch arguments
- PID
- Version of the monitored target
- Time of the last heartbeat received
- Exit time
- Exit code
- Restart count
- Whether a dump exists
- Whether a restart happened
With just this, you can see fairly clearly:
- Whether it truly crashed
- Whether it was an OS shutdown
- Whether the user closed it
- Whether it hung and got killed
- How many times it looped through restarts
flowchart TB
accTitle: What the watchdog record lets you distinguish
accDescr: A diagram showing that when a monitoring process records exit codes, exit times, the last heartbeat received, and restart counts, you can tell from outside whether the app really crashed, whether it was an OS shutdown, whether the user closed it, whether it was killed after a hang, or whether it was a restart loop.
t1["Exit code and exit time"] --> t4["Distinguishable from outside"]
t2["Last heartbeat received"] --> t4
t3["Restart count"] --> t4
t4 -.-> t5["Crash, OS shutdown, or user-initiated exit"]
Figure 21: With the watchdog record in place, the kind of termination can be told apart from outside.
9.2 Cases Where It Especially Fits
Separation is worth actively considering in cases like:
- A worker carrying a vendor SDK
- Image processing / video processing / device I/O
- Monitoring or polling parent loops
- Script or plugin execution
- Hosting legacy COM / ActiveX assets
- 64-bit / 32-bit bridging and interop
Confining dangerous work to a single worker makes both log design and recovery design easier.
10. Common Anti-Patterns
This section collects the design-level pitfalls. The procedure-level question of “what must not happen inside a crash handler” is covered in 5.2, so if you are mid-implementation, look there. Where the two overlap (HTTP transmission in 10.3, for instance), 5.2 explains why doing it inside the handler is a problem, while this section describes what that then does to you in operations.
10.1 catch (Exception) That Logs and Continues
The most common, and the most dangerous.
- Partial changes remain
- Shared state gets corrupted
- Follow-on failures multiply
- The true point of origin gets blurred
More often than not, you gain one extra log line and, in exchange, the incident drags on.
10.2 Trusting Only the Async Logger’s Queue
Asynchronous logging itself is not bad. The problem is pushing onto the same queue even on the fatal path and calling it done.
If the worker stops at the moment of the crash, the whole queue goes with it.
It is safer to keep an escape hatch where the fatal path alone writes directly.
10.3 Sending HTTP from the Crash Handler
An easy thing to reach for, and quite dangerous.
- DNS
- TLS
- Proxy
- Authentication
- Timeouts
- Retry waits
All of it rides on the crashed context.
Send after restart.
10.4 Dumps Exist, but Do Not Connect to the Regular Log
This is common.
- No session in the dump file name
- No PID / session on the log side
- No PID on the watchdog side
- Build numbers do not match
The result: the three pieces of evidence look like three unrelated stories.
flowchart TB
accTitle: Evidence that fails to connect
accDescr: A diagram showing that when the dump file name carries no session, the log carries no PID or session, and the build numbers do not match, the dump, the log, and the watchdog record end up looking like three separate incidents.
u1["No session in the dump name"] --> u4["Three pieces of evidence look unrelated"]
u2["No PID / session in the log"] --> u4
u3["Build numbers do not match"] --> u4
Figure 22: When the key IDs are missing, hard-won evidence stops connecting to the rest.
10.5 Keeping WinForms / WPF Alive via the Unhandled-Exception Events
Superficially the app “stops crashing,” so at first everyone is pleased. But in reality, it tends to create a zombie state where:
- Only the screen survives
- The worker is dead
- Only the button stays enabled
- Nobody knows whether the save succeeded
10.6 Not Watching the Native Termination Paths
If SetUnhandledExceptionFilter alone makes you feel safe, you will miss:
- invalid parameter
- purecall
- terminate
- fast fail
In native C++, it is better to stay conscious of the CRT / C++ runtime termination paths, not just SEH.
11. The Minimum Adoption Checklist
If you satisfy the following, you are in quite practical shape.
- The regular log records one event per line
- Every log line has UTC, PID, TID, version, and session
ProcessStartandProcessExitare recorded- Important boundary events are flushed synchronously
- A dedicated final-crash-marker file exists
- The fatal path does not go through the async logger
- WER LocalDumps is configured per application
- The dump destination’s ACLs have been verified
- PDBs and shipped binaries are archived
- The next startup can detect the previous abnormal exit
- Compression / upload / notification happen after restart or in a separate process
- In native C++, invalid parameter / purecall / terminate are accounted for
- You crashed it deliberately on a test machine and confirmed the evidence is really left behind
The last line is especially important. Designing it is not enough - you must run the “does it actually capture everything” test.
12. How Far to Test
The items worth verifying, in a table.
| Test | What to confirm |
|---|---|
| Managed unhandled exception | Do the regular log, fatal marker, and dump all show up? |
| UI thread exception | Does the WinForms / WPF event path behave as designed? |
| Worker thread exception | Does it reach AppDomain.UnhandledException? Can the watchdog detect it? |
| Native exception | Is the WER dump really captured? |
| invalid parameter / terminate | Is minimal recording left even on CRT / C++ runtime paths? |
| Forced kill | Even if in-process can do nothing, does the watchdog record the unexpected exit? |
| Restart | Do notification, collection, and upload work after the next startup? |
What matters is not “a log should appear if an exception flies,” but confirming “under this condition, this file is left behind.”
13. Summary
If you want a Windows app to leave the information needed for investigation even when it dies from an exception caused by a programming error, the axes of thinking are quite simple.
- Do not pin your hopes on the crashing process alone
- Split into the regular log, the final crash marker, and OS / separate-process evidence
- At crash time, write only a short record locally
- Defer heavy work to after restart or to a separate process
- Build on WER LocalDumps
- Default to record-and-exit rather than continuation
In the end, “building a configuration that stays traceable even without the last line” beats “trying hard at the last line.”
flowchart TB
accTitle: A setup that does not depend on the last line
accDescr: A diagram showing that building a setup that stays traceable without the last line beats trying hard at the last line, while the final crash marker is still written briefly to a separate file and the WER dump and the regular log up to that moment carry the primary evidence.
v0["What to aim for"] --> v2["A setup traceable without the last line"]
v2 --> v3["The marker stays short, in a separate file"]
v2 --> v4["Primary evidence: the dump and the regular log"]
Figure 23: Rather than trying hard at the last line, build a setup that works without it.
Still, you do want that last line - so write the final crash marker briefly, to a separate file. And entrust the true primary evidence to the WER dump and the regular log up to the moment before. That is a very stable way of working in real-world Windows application practice.
Related Articles
- An Introduction to Collecting Windows Crash Dumps - WER/ProcDump/WinDbg
- A Decision Table for Whether to Exit or Continue After an Unexpected Exception
References
- Microsoft Learn: Collecting User-Mode Dumps https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps
- Microsoft Learn: Using WER https://learn.microsoft.com/en-us/windows/win32/wer/using-wer
- Microsoft Learn: MiniDumpWriteDump function https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/nf-minidumpapiset-minidumpwritedump
- Microsoft Learn: SetUnhandledExceptionFilter function https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-setunhandledexceptionfilter
- Microsoft Learn: System.AppDomain.UnhandledException event https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-appdomain-unhandledexception
- Microsoft Learn: Application.ThreadException Event https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.application.threadexception
- Microsoft Learn: Application.DispatcherUnhandledException Event https://learn.microsoft.com/en-us/dotnet/api/system.windows.application.dispatcherunhandledexception
- Microsoft Learn: TaskScheduler.UnobservedTaskException Event https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskscheduler.unobservedtaskexception
- Microsoft Learn: Environment.FailFast https://learn.microsoft.com/en-us/dotnet/api/system.environment.failfast
- Microsoft Learn: Registering for Application Recovery https://learn.microsoft.com/en-us/windows/win32/recovery/registering-for-application-recovery
- Microsoft Learn: RegisterApplicationRecoveryCallback https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-registerapplicationrecoverycallback
- Microsoft Learn: WerRegisterFile https://learn.microsoft.com/en-us/windows/win32/api/werapi/nf-werapi-werregisterfile
- Microsoft Learn: _set_invalid_parameter_handler https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/set-invalid-parameter-handler-set-thread-local-invalid-parameter-handler
- Microsoft Learn: _set_purecall_handler https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/get-purecall-handler-set-purecall-handler
- Microsoft Learn: set_terminate https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/set-terminate-crt
- Microsoft Learn: __fastfail https://learn.microsoft.com/en-us/cpp/intrinsics/fastfail
- Microsoft Learn: FileStream.Flush(Boolean) https://learn.microsoft.com/en-us/dotnet/api/system.io.filestream.flush
- Microsoft Learn: !analyze (WinDbg) https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-analyze
- Microsoft Learn: .ecxr (Display Exception Context Record) https://learn.microsoft.com/en-us/windows-hardware/drivers/debuggercmds/-ecxr–display-exception-context-record-
- Microsoft Learn: Symbol path for Windows debuggers https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/symbol-path
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
An Introduction to Collecting Windows Crash Dumps - WER/ProcDump/WinDbg
To chase hard-to-reproduce Windows application crashes, we walk through when to use WER LocalDumps, ProcDump, MiniDumpWriteDump, and WinD...
Incident Response Doesn't End at Recovery — A Postmortem (Recurrence Prevention) Template for Small Development Teams
Treating an incident as over once it's fixed and apologized for guarantees you'll repeat it. This article translates the blameless postmo...
Sleep, Hibernation, Modern Standby, and Long-Running Apps — Designing Around 'It Stopped Overnight'
Why a long-running Windows app can end up 'stopped by the time you check it in the morning,' worked through from the differences between ...
When You Inherit a System With No Source Code and No Documentation — A Practical Playbook for Keeping It Running
A practical playbook for starting operations and maintenance on a business system that has no source code and no specifications. Covers p...
MAX_PATH and Windows Path/Filename Pitfalls — the 260-Character Limit, Reserved Names, Trailing Dots, and Case Sensitivity
A rundown of the path and filename limits behind the classic 'file not found' bug. Covers the breakdown of MAX_PATH=260, enabling long pa...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Bug Investigation & Long-Run Failures
Topic page for intermittent failures, communication diagnosis, long-run crashes, and failure-path test foundations.
Where This Topic Connects
This article connects naturally to the following service pages.
Bug Investigation & Root Cause Analysis
Crashes that only occur in customer environments, low-reproducibility abnormal terminations, and root-cause analysis by cross-referencing dumps with logs are a good fit for bug investigation and root cause analysis.
Windows App Development
How to design regular logging, WER, and watchdogs for WPF, WinForms, resident apps, and Windows services is directly connected to Windows application development itself.
Frequently Asked Questions
Common questions about the topic of this article.
- Can a crashing app reliably write its own log?
- No. Once stack corruption, memory corruption, fast fail, forced termination, and power loss are in scope, the final in-process log is fundamentally best effort. In practice, split the evidence into three layers - a chronological log written during normal operation, a final crash marker written at the moment of the crash, and crash evidence left by the OS or a separate process - so that nothing depends on the inside of the crashing process alone. The safest combination is regular log + final crash marker + WER LocalDumps.
- What must you never do inside a crash handler?
- Anything heavy. Resolving a logger from a DI container, async/await, waiting on locks, building complex JSON, touching COM objects, showing UI dialogs, compressing, and HTTP/SMTP/Slack transmission are all land mines with high probability. Keep the handler to four things only: prevent reentry, write one line, flush, and exit. Heavy post-processing such as compression, upload, and notification is deferred to the next startup or a separate process.
- How should WER LocalDumps be configured?
- Under the registry key HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\<AppName>.exe, set DumpFolder (a dedicated folder), DumpCount (around 5 to 10), and DumpType (2 on development machines; 1 or 2 in the field, depending on disk space and confidentiality requirements). The critical part is verifying the ACL on the destination: the classic failure is pointing a Windows service or a restricted account at a folder it cannot write to, so nothing ever lands there. Dump collection and the archiving of PDBs and shipped binaries also go together - miss either one and you will not be able to read the dump later.
- Is it safe to swallow the exception in WPF's DispatcherUnhandledException and keep running?
- It is dangerous against unexpected exceptions that originate in programming errors. Handled=true lets the app continue superficially, but it tends to create a zombie state: the screen is still there while the worker is dead, the button is still enabled while nobody knows whether the save succeeded. Use unhandled-exception events as an entry point for recording rather than as a recovery device: once the final crash marker is written, terminate instead of continuing, and let the WER dump and the regular log up to that moment carry the primary evidence. For termination, consider an immediate-termination API such as Environment.FailFast.