Introduction
Capturing console application exit in C# allows you to perform cleanup (closing files, flushing logs, releasing resources) before the process terminates. The primary mechanism is Console.CancelKeyPress for Ctrl+C handling. For other exit scenarios (window close, system shutdown), use the Win32 SetConsoleCtrlHandler API via P/Invoke. AppDomain.CurrentDomain.ProcessExit fires on normal exit but has a strict time limit.
Console.CancelKeyPress (Ctrl+C / Ctrl+Break)
1using System;
2
3class Program
4{
5 static void Main()
6 {
7 Console.CancelKeyPress += (sender, args) =>
8 {
9 Console.WriteLine("Ctrl+C detected. Cleaning up...");
10
11 // Prevent immediate termination
12 args.Cancel = true;
13
14 // Perform cleanup
15 CleanUp();
16 };
17
18 Console.WriteLine("Running. Press Ctrl+C to exit.");
19 while (true)
20 {
21 // Main application loop
22 System.Threading.Thread.Sleep(1000);
23 Console.Write(".");
24 }
25 }
26
27 static void CleanUp()
28 {
29 Console.WriteLine("\nSaving state...");
30 Console.WriteLine("Closing connections...");
31 Environment.Exit(0);
32 }
33}
Setting args.Cancel = true prevents the default behavior (immediate termination) and lets your code run cleanup logic before calling Environment.Exit().
AppDomain.ProcessExit (Normal Exit)
1using System;
2
3class Program
4{
5 static void Main()
6 {
7 AppDomain.CurrentDomain.ProcessExit += (sender, args) =>
8 {
9 Console.WriteLine("Process is exiting. Running cleanup...");
10 // Flush logs, close files, dispose resources
11 // WARNING: This handler has roughly a 2 second time limit
12 };
13
14 Console.WriteLine("Application running...");
15 // Application does work...
16
17 Console.WriteLine("Exiting normally.");
18 // ProcessExit fires after Main() returns
19 }
20}
ProcessExit fires when the application exits normally (end of Main, Environment.Exit). It does NOT fire on forced kills (taskkill /F, kill -9).
SetConsoleCtrlHandler (All Exit Types)
For comprehensive exit detection including window close and system shutdown, use the Win32 API:
1using System;
2using System.Runtime.InteropServices;
3
4class Program
5{
6 // Win32 API import
7 [DllImport("Kernel32")]
8 private static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate handler, bool add);
9
10 private delegate bool ConsoleCtrlDelegate(CtrlTypes type);
11
12 enum CtrlTypes
13 {
14 CTRL_C_EVENT = 0,
15 CTRL_BREAK_EVENT = 1,
16 CTRL_CLOSE_EVENT = 2,
17 CTRL_LOGOFF_EVENT = 5,
18 CTRL_SHUTDOWN_EVENT = 6
19 }
20
21 private static bool ConsoleCtrlHandler(CtrlTypes type)
22 {
23 switch (type)
24 {
25 case CtrlTypes.CTRL_C_EVENT:
26 Console.WriteLine("Ctrl+C pressed");
27 break;
28 case CtrlTypes.CTRL_BREAK_EVENT:
29 Console.WriteLine("Ctrl+Break pressed");
30 break;
31 case CtrlTypes.CTRL_CLOSE_EVENT:
32 Console.WriteLine("Console window closing");
33 break;
34 case CtrlTypes.CTRL_LOGOFF_EVENT:
35 Console.WriteLine("User logging off");
36 break;
37 case CtrlTypes.CTRL_SHUTDOWN_EVENT:
38 Console.WriteLine("System shutting down");
39 break;
40 }
41
42 CleanUp();
43 return true; // Signal handled
44 }
45
46 static void Main()
47 {
48 SetConsoleCtrlHandler(ConsoleCtrlHandler, true);
49
50 Console.WriteLine("Running. Try closing the window or pressing Ctrl+C.");
51 while (true)
52 {
53 System.Threading.Thread.Sleep(1000);
54 }
55 }
56
57 static void CleanUp()
58 {
59 Console.WriteLine("Performing cleanup...");
60 // Save state, flush buffers, close connections
61 }
62}
Using CancellationToken (Modern Approach)
1using System;
2using System.Threading;
3using System.Threading.Tasks;
4
5class Program
6{
7 static async Task Main()
8 {
9 using var cts = new CancellationTokenSource();
10
11 Console.CancelKeyPress += (sender, args) =>
12 {
13 args.Cancel = true;
14 cts.Cancel(); // Signal cancellation
15 };
16
17 AppDomain.CurrentDomain.ProcessExit += (sender, args) =>
18 {
19 cts.Cancel();
20 };
21
22 try
23 {
24 await RunApplicationAsync(cts.Token);
25 }
26 catch (OperationCanceledException)
27 {
28 Console.WriteLine("Application cancelled. Cleaning up...");
29 }
30 finally
31 {
32 await CleanUpAsync();
33 }
34 }
35
36 static async Task RunApplicationAsync(CancellationToken token)
37 {
38 while (!token.IsCancellationRequested)
39 {
40 await Task.Delay(1000, token);
41 Console.Write(".");
42 }
43 }
44
45 static async Task CleanUpAsync()
46 {
47 Console.WriteLine("\nFlushing data...");
48 await Task.Delay(500); // Simulate async cleanup
49 Console.WriteLine("Done.");
50 }
51}
This is the recommended pattern for modern .NET applications. CancellationToken propagates through async methods, allowing graceful shutdown of all background tasks.
.NET Generic Host (ASP.NET Core / Worker Services)
1using Microsoft.Extensions.Hosting;
2
3var host = Host.CreateDefaultBuilder(args)
4 .ConfigureServices(services =>
5 {
6 services.AddHostedService<MyWorker>();
7 })
8 .Build();
9
10await host.RunAsync();
11
12// MyWorker.cs
13public class MyWorker : BackgroundService
14{
15 protected override async Task ExecuteAsync(CancellationToken stoppingToken)
16 {
17 while (!stoppingToken.IsCancellationRequested)
18 {
19 Console.WriteLine("Working...");
20 await Task.Delay(1000, stoppingToken);
21 }
22 }
23
24 public override async Task StopAsync(CancellationToken cancellationToken)
25 {
26 Console.WriteLine("Worker stopping. Cleaning up...");
27 await base.StopAsync(cancellationToken);
28 }
29}
The Generic Host handles Ctrl+C, SIGTERM, and process exit automatically, calling StopAsync on all hosted services.
Linux/macOS Signal Handling (.NET 6+)
1using System;
2using System.Runtime.InteropServices;
3
4// Handle SIGTERM (sent by Docker, systemd, Kubernetes)
5PosixSignalRegistration.Create(PosixSignal.SIGTERM, ctx =>
6{
7 Console.WriteLine("SIGTERM received");
8 ctx.Cancel = true; // Prevent default termination
9 // Perform cleanup
10});
11
12// Handle SIGINT (Ctrl+C, alternative to Console.CancelKeyPress)
13PosixSignalRegistration.Create(PosixSignal.SIGINT, ctx =>
14{
15 Console.WriteLine("SIGINT received");
16 ctx.Cancel = true;
17});
Common Pitfalls
ProcessExit time limit: The ProcessExit handler has approximately 2 seconds to complete. Long cleanup operations get cut off. Move heavy cleanup to before Main returns.
args.Cancel = true only works for Ctrl+C: Setting Cancel in CancelKeyPress prevents termination for Ctrl+C, but CTRL_CLOSE_EVENT (window close) gives you only about 5 seconds regardless.
SetConsoleCtrlHandler is Windows-only: The P/Invoke approach does not work on Linux/macOS. Use PosixSignalRegistration (.NET 6+) for cross-platform signal handling.
Environment.Exit() skips finally blocks: Calling Environment.Exit(0) from a handler terminates immediately, skipping finally blocks in other threads. Use CancellationToken for cooperative shutdown.
SIGKILL / taskkill /F cannot be caught: kill -9 and taskkill /F terminate the process immediately with no opportunity for cleanup. Design your application to recover from unclean shutdowns.
Summary
Use Console.CancelKeyPress for Ctrl+C handling with args.Cancel = true to prevent immediate exit
Use AppDomain.CurrentDomain.ProcessExit for normal exit cleanup (2-second limit)
Use SetConsoleCtrlHandler (Windows) or PosixSignalRegistration (.NET 6+, cross-platform) for comprehensive signal handling
The modern pattern uses CancellationToken for cooperative async shutdown
.NET Generic Host handles exit signals automatically for hosted services
No mechanism can catch forced kills (kill -9, taskkill /F)