C#
console application
exit event
application termination
event handling

Capture console exit C

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

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)

csharp
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)

csharp
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:

csharp
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)

csharp
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)

csharp
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+)

csharp
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)

Course illustration
Course illustration

All Rights Reserved.