Designing Windows Apps to Leave Logs and Dumps When They Crash

· Updated: · · 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.

The final in-process log is best effortA 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.Stack corruption / memory corruptionFinal in-process logFast fail / forced terminationPower lossFundamentally best effortGuaranteed 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:

  1. The regular, chronological log
  2. The final crash marker at the moment of the crash
  3. 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 ThreadException or WPF’s DispatcherUnhandledException to 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.”

Dividing the roles before, during, and after the crashA 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.Before the crash: keep a regular logAt the crash: write only a short local recordAfter the crash: compress, upload, notifyDo 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.

Crash-time log and dump design for Windows appsDiagram 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 processrequiresrequiresrequiresrequiresrequiresrecommended forshould come beforerequiresrecommended fornot recommended fornot recommended fornot recommended forusesrecommended forrecommended forrequiresrequiresverified byimplementsusesusesshould come beforeimplementsrecommended forCrash-time logging and evidence designWER LocalDumpsApplication Log (Time-Series)Fatal Crash Marker FileSEH (Structured Exception Handling)CRT/C++ runtime termination pathsWatchdog ProcessDemanding 24/7 and Device-Control RequirementsDeferred Processing After RestartSession ID CorrelationAppDomain.UnhandledExceptionApplication.ThreadException (WinForms)Continuing After a Program-Bug ExceptionApplication.DispatcherUnhandledException (WPF)TaskScheduler.UnobservedTaskExceptionEnvironment.FailFastWindows Application Event LogDump Folder ACLCrash dumpPDB (Program Database)WinDbgMiniDumpWriteDumpLog Attachment via WerRegisterFile

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.”

Very little is possible in the final handlerA 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.Runs in the broken thread contextStack and heap are unsafeWaiting on a lock can hangLogger dependencies may be brokenA place where very little is possible

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.”

The unhandled-exception event is the final notificationA 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.Unhandled-exception eventLimit it to a short recordUse it as the final notificationNot a safe recovery pointForcing continuation keeps a half-broken process alive

Figure 4: The unhandled-exception event is the entry point for recording, not the place for recovery.

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.

Next healthy process startedwatchdog processWindows sideApplication processexception thrownprocess exitsprocess exitsCompress / upload / notifydetect the previous abnormal exitRecord exit code and exit timedecide on restartWER LocalDumpsdump saved out of processRegular logappend-only timelineFinal crash markerwrite one line, then exitFixed local folder

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.

Division of work in the stronger configurationA 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.Stronger requirements (24/7, device control, and so on)worker: the main workloadwatchdog: startup supervision, exit recording, restartConfigure WER LocalDumps on the worker sideNext 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.”

One log line links three pieces of evidenceA 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.One line of the regular logpid / session lead to the dumpver / commit lead to the buildThree files can be cross-referenced

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 Information events: buffering is fine
  • Warning and 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.

Choosing the write mode by levelA 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.Log writesInformation: buffering is fineWarning and above: flush earlyBoundary events: write synchronouslyBusiness 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>.jsonl The regular chronological log
  • fatal-last.log or fatal-<session>.log Dedicated to the final crash marker

Just having “where the last line goes” be unambiguous helps enormously in the field.

Separate the regular log from the fatal markerA 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.insteadEverything in one rolling logLost to rotation, a queue, or a dead loggerSplit into a regular log and a fatal markerWhere 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.UnhandledException
    • Application.ThreadException
    • DispatcherUnhandledException
    • SetUnhandledExceptionFilter
    • _set_invalid_parameter_handler
    • set_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.

The marker exists to pin the entry pointA 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.Final crash markerWhich hook, exception type, sessionRegular log name and dump folderThe investigation entry point is pinnedLeave 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.

  1. Prevent reentry
  2. Write one line
  3. Flush
  4. 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”.

The four steps of a crash handlerA 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.1. Prevent reentry2. Write one line3. Flush4. ExitThis 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 NullReferenceException or InvalidOperationException that occurred mid-update of shared state
  • Unexpected exceptions on the UI thread
  • Unexpected exceptions that leaked out of monitoring or parent loops
  • AccessViolationException
  • StackOverflowException
  • 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.

A recording device, not a recovery deviceA 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.Exception from a programming errorSurvive half-brokenRecord and exitWorse for diagnosis and for operationsConsider 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.

  1. Interlocked.Exchange prevents reentry
  2. String concatenation alone writes one line
  3. Flush(true) pushes it to disk
  4. The UnhandledException path 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.

Choose where to call FailFastA 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.Environment.FailFastWrites to the event log, then exits immediatelyOne more piece of evidenceCalled on the unhandled-exception pathThe 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 = true allows 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.

Do not use UI events as life supportA 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.Unhandled exception on the UI threadSuperficial continuation is possibleScreen state and internal state drift apartUse it as an entry point for recordingTerminate 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 Task design 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_handler
  • set_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
SEH alone leaves gapsA 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.Only SetUnhandledExceptionFilterMisses the CRT termination pathsAlso catch invalid parameter / purecall / terminateWrite a minimal record, then terminate reliablyLeave 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.

Why WER LocalDumps is strongA 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.WER LocalDumpsThe OS side persists the dumpConfigurable per applicationPrimary evidence moves outside the processThread, 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
Preventing a dump destination that never receives anythingA 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.to prevent itServices and restricted accountsThe destination folder is not writableThe dump never landsPre-create the folder and run a write testCheck 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:

  1. Local regular log
  2. Local fatal marker
  3. Local dump
  4. 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.

Dump collection and PDB archiving are a setA 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.soOnly the dump is keptNo PDBs or binaries from that buildUnreadable laterArchive the shipped binaries and the PDBsKeep 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.

  1. Gather the PDBs and shipped binaries from the same build as that dump into a single folder
  2. 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.

The first ten minutes with a dumpA 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.noGather PDBs and binaries from the same buildOpen the dump in WinDbgMove to the exception context and analyzeForget .ecxr and you read the WER wait stackDo function names and line numbers appearPDBs 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 tail of 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.

Taking a dump from a separate helper processA 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.helperworker processhelperworker processThe helper stays healthy even when the worker is brokenNotify the anomaly via an event or a pipeTake a dump of the workerBundle the tail of the logs and the config filesPlace them in the upload queue after exit

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
What the watchdog record lets you distinguishA 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.Exit code and exit timeDistinguishable from outsideLast heartbeat receivedRestart countCrash, 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.

Evidence that fails to connectA 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.No session in the dump nameThree pieces of evidence look unrelatedNo PID / session in the logBuild numbers do not match

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
  • ProcessStart and ProcessExit are 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.”

A setup that does not depend on the last lineA 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.What to aim forA setup traceable without the last lineThe marker stays short, in a separate filePrimary 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.

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

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

These topic pages place the article in a broader service and decision context.

This article connects naturally to the following service pages.

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.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog