skip to content
Khalid Abuhakmeh logo Khalid Abuhakmeh

.NET Debugger.Launch for macOS and Linux

Debugger.Launch only works on Windows. Get a cross-platform C# workaround that lets you pause execution and attach your debugger on macOS and Linux too.

6 min read Updated

Updated October 2026 — Verified against .NET 10. Debugger.Launch() is still a no-op on macOS and Linux, so the workaround below still applies. I’ve also added the modern, officially-supported way to attach a debugger on these platforms (Rider and VS Code “Attach to Process”).

Short answer: System.Diagnostics.Debugger.Launch() only launches a debugger on Windows. On macOS and Linux it returns true but does nothing. To pause a running .NET process and attach your debugger cross-platform, either use your IDE’s Attach to Process feature (the recommended path) or have your code poll Debugger.IsAttached in a short loop until you attach manually. This post shows both.

While we’re all benefiting from the push towards “One .NET”, the reality is that there will always be differences in operating systems. Windows, macOS, and Linux are, in fact, different operating systems and have their advantages and disadvantages. The difference is a truism easily overlooked when writing C# code and compiling our apps to an intermediate language. In general, everything works, but sometimes we get a rude reminder. One of those reminders is the Debugger class. This post will show you an admittedly janky way of achieving the same result as Debugger.Launch across operating systems.

What Is Debugger.Launch?

.NET started life as a Windows-only platform. Therefore, the .NET team could make assumptions about the environment and add functionality specific to the Windows operating system. For example, when calling Debugger.Launch from .NET code within a Windows environment, the .NET framework will make a system-level call to pause the current process and then launch and attach the debugger. In most cases, that debugger is typically Visual Studio or JetBrains Rider. Developers usually use this method to isolate an issue that is hard to reproduce, but they have a general idea of when or where it might appear.

Sounds helpful, right? Well, only for Windows users. Let’s take a look at the Launch method.

// Launch launches & attaches a debugger to the process. If a debugger is already attached,
// nothing happens.
//
public static bool Launch() => IsAttached ? true : LaunchInternal();
[DllImport(RuntimeHelpers.QCall, CharSet = CharSet.Unicode)]
private static extern bool LaunchInternal();

For those unfamiliar with native APIs, the attribute of DllImport calls down to native code compiled to the current operating system. On Windows, the operating system implements these APIs as you would expect. However, on macOS and Linux, the Windows native method is replaced by a no-op call, which, as you may have guessed, does absolutely nothing.

This is still true as of .NET 10. The official Debugger.Launch documentation says debugger launch is supported only on Windows, and on Unix-like systems Launch() returns true without launching anything. You can prove it in a few lines:

using System.Diagnostics;
// On macOS/Linux this prints: Launch() = True, IsAttached = False
Console.WriteLine($"Launch() = {Debugger.Launch()}");
Console.WriteLine($"IsAttached = {Debugger.IsAttached}");
Terminal window
OS: macOS 27.2.0
Launch() = True
IsAttached = False

The call reports success but no debugger ever attaches. Debugger.Break() isn’t a portable substitute either. It signals an already-attached debugger rather than launching one.

Well, what can folks on macOS and Linux do? There’s a proper, officially-supported answer and a janky one. Let’s cover both.

If the process is already running, attach your IDE’s debugger to the live process. No code changes required.

  • JetBrains Rider: Choose Run | Attach to Process, pick your process, and select .NET to filter the list to managed processes. Rider also offers Attach to an Unstarted Process, which watches for a matching process to start and attaches automatically. That one is handy when you need to catch startup code. See the Rider “Attach to process” docs and my post on being more productive with the JetBrains Rider debugger for more debugging tricks.
  • Visual Studio Code (C# Dev Kit): Open the Command Palette and run Debug: Attach to a .NET 5+ or .NET Core process, then pick your process. See the VS Code C# attach docs.

If you need the process to pause at startup so you can attach before your code runs, .NET’s diagnostic port can suspend startup with an environment variable:

Terminal window
DOTNET_DefaultDiagnosticPortSuspend=1 dotnet run

One caveat: this waits for a diagnostic client (like dotnet-trace) to connect to the diagnostic port, not specifically a managed debugger. It’s great for diagnostics tooling, but it isn’t a drop-in “wait for my IDE debugger” switch. For that last scenario, pausing your own code until you attach, the janky workaround below is still the most pragmatic option.

The Janky Debugger.Launch for macOS and Linux

While Debugger.Launch doesn’t work, the Debugger.IsAttached property does. When you attach the debugger, the property value will change. We can write a method that waits for the debugger to connect to the currently paused process. This is the same pattern the .NET test platform (VSTest) uses internally to wait for a debugger, so you’re in good company.

using System.Diagnostics;
public static class Janky
{
public static async Task<bool> WaitForDebugger(TimeSpan? limit = null)
{
limit ??= TimeSpan.FromSeconds(30);
var source = new CancellationTokenSource(limit.Value);
Console.WriteLine($"◉ Waiting {limit.Value.TotalSeconds} secs for debugger (PID: {Environment.ProcessId})...");
try
{
await Task.Run(async () => {
while (!Debugger.IsAttached) {
await Task.Delay(TimeSpan.FromMilliseconds(100), source.Token);
}
}, source.Token);
}
catch (OperationCanceledException)
{
// it's ok
}
Console.WriteLine(Debugger.IsAttached
? "✔ Debugger attached"
: "✕ Continuing without debugger");
return Debugger.IsAttached;
}
}

Now you can call WaitForDebugger in place of Debugger.Launch. There’s no point calling the no-op on these platforms, so drop it entirely.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => Debugger.IsAttached);
// Pause here, then attach via Rider/VS Code "Attach to Process"
await Janky.WaitForDebugger();
app.Run();

To avoid an infinite loop, I decided to add a configurable timeout period through the method’s arguments. We can see the debugger waiting in the console output when running our app. The technique also outputs the process id for user-friendliness. Let’s see an unsuccessful attempt first.

Terminal window
◉ Waiting 30 secs for debugger (PID: 14302)...
✕ Continuing without debugger

Now let’s see a successful attempt to attach the debugger.

Terminal window
◉ Waiting 30 secs for debugger (PID: 14644)...
✔ Debugger attached

Note, we stop waiting as soon as the debugger is attached, so there is no need to worry about wasting time waiting for the total time limit to elapse. A couple of guardrails worth keeping: gate this behind an explicit debug flag or #if DEBUG so it never runs in production, and always keep the timeout so an unattended process can’t hang forever.

For more cross-platform .NET tooling tips, see being more productive with the JetBrains Rider debugger and my C# cheat sheet for more handy System.Diagnostics snippets.

I hope you found this post helpful, and as always, thanks for reading and sharing. If you have a different approach, please let me know on Mastodon @khalidabuhakmeh.

Comments

Related Posts