Debugging
Debug.WriteLine
Release Build
Software Development
.NET

Debug.WriteLine in release build

Master System Design with Codemia

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

Introduction

Debug.WriteLine is completely removed from Release builds in .NET. The Debug class in System.Diagnostics is decorated with [Conditional("DEBUG")], which means the compiler strips all Debug.* calls when the DEBUG symbol is not defined. In Release configuration, DEBUG is not defined by default, so Debug.WriteLine statements produce zero overhead — no method call, no string formatting, no output. For logging in Release builds, use Trace.WriteLine, a logging framework (Serilog, NLog), or ILogger.

How It Works

csharp
1using System.Diagnostics;
2
3public void ProcessOrder(Order order)
4{
5    Debug.WriteLine($"Processing order: {order.Id}");  // REMOVED in Release
6    Debug.Assert(order != null, "Order cannot be null"); // REMOVED in Release
7
8    // Business logic...
9    Trace.WriteLine($"Order processed: {order.Id}");   // KEPT in Release
10}

Compiler Behavior

csharp
1// In Debug configuration (DEBUG symbol defined):
2Debug.WriteLine("Hello");  // Compiled and executed
3
4// In Release configuration (DEBUG symbol NOT defined):
5// Debug.WriteLine("Hello");  // Entire line is removed by the compiler
6// The compiled IL contains no trace of this call

The [Conditional("DEBUG")] attribute on the Debug class tells the C# compiler to exclude calls to its methods when the DEBUG preprocessor symbol is absent.

Debug vs Trace vs ILogger

csharp
1using System.Diagnostics;
2using Microsoft.Extensions.Logging;
3
4public class OrderService
5{
6    private readonly ILogger<OrderService> _logger;
7
8    public OrderService(ILogger<OrderService> logger)
9    {
10        _logger = logger;
11    }
12
13    public void Process(Order order)
14    {
15        // Debug.WriteLine — REMOVED in Release
16        Debug.WriteLine($"Debug: Processing {order.Id}");
17
18        // Trace.WriteLine — KEPT in Release (controlled by TRACE symbol)
19        Trace.WriteLine($"Trace: Processing {order.Id}");
20
21        // ILogger — KEPT in Release, configurable level
22        _logger.LogDebug("Processing {OrderId}", order.Id);
23        _logger.LogInformation("Order {OrderId} processed", order.Id);
24    }
25}
MethodDebug BuildRelease BuildConfigurable
Debug.WriteLineActiveRemovedNo (compile-time)
Trace.WriteLineActiveActiveListeners in config
ILogger.LogDebugActiveActiveLog level in config
Console.WriteLineActiveActiveNo (always runs)

Verifying the Behavior

csharp
1public class DebugTest
2{
3    public static void Main()
4    {
5        Console.WriteLine("Console: Always visible");
6        Debug.WriteLine("Debug: Only in Debug builds");
7        Trace.WriteLine("Trace: In both builds");
8
9        #if DEBUG
10        Console.WriteLine("Compiled with DEBUG symbol");
11        #else
12        Console.WriteLine("Compiled WITHOUT DEBUG symbol");
13        #endif
14    }
15}
bash
1# Debug build
2dotnet run -c Debug
3# Console: Always visible
4# Debug: Only in Debug builds    (visible in Output window)
5# Trace: In both builds
6# Compiled with DEBUG symbol
7
8# Release build
9dotnet run -c Release
10# Console: Always visible
11# Trace: In both builds
12# Compiled WITHOUT DEBUG symbol

Using Trace for Release Logging

csharp
1using System.Diagnostics;
2
3// Add trace listeners in app.config
4// <system.diagnostics>
5//   <trace autoflush="true">
6//     <listeners>
7//       <add name="file" type="System.Diagnostics.TextWriterTraceListener"
8//            initializeData="app.log" />
9//     </listeners>
10//   </trace>
11// </system.diagnostics>
12
13public void ProcessData()
14{
15    Trace.TraceInformation("Starting data processing");
16    Trace.TraceWarning("Large dataset detected");
17    Trace.TraceError("Processing failed for record 42");
18
19    // Trace with category
20    Trace.WriteLine("Custom message", "MyCategory");
21}

Trace is controlled by the TRACE preprocessor symbol, which is defined in both Debug and Release configurations by default.

Custom Conditional Methods

csharp
1// Create your own conditionally compiled methods
2[Conditional("DEBUG")]
3public static void DebugLog(string message)
4{
5    Console.WriteLine($"[DEBUG] {DateTime.Now:HH:mm:ss} {message}");
6    File.AppendAllText("debug.log", $"{DateTime.Now}: {message}\n");
7}
8
9[Conditional("VERBOSE")]
10public static void VerboseLog(string message)
11{
12    Console.WriteLine($"[VERBOSE] {message}");
13}
14
15// Usage
16DebugLog("Processing started");    // Removed in Release
17VerboseLog("Step 1 complete");     // Only if VERBOSE symbol is defined
xml
1<!-- Define custom symbols in .csproj -->
2<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
3  <DefineConstants>DEBUG;TRACE;VERBOSE</DefineConstants>
4</PropertyGroup>
5<PropertyGroup Condition="'$(Configuration)' == 'Release'">
6  <DefineConstants>TRACE</DefineConstants>
7</PropertyGroup>

Modern Logging with ILogger

csharp
1// Program.cs (.NET 6+)
2var builder = WebApplication.CreateBuilder(args);
3
4// Configure logging levels per environment
5// appsettings.Development.json
6// { "Logging": { "LogLevel": { "Default": "Debug" } } }
7
8// appsettings.Production.json
9// { "Logging": { "LogLevel": { "Default": "Warning" } } }
10
11var app = builder.Build();
12
13app.MapGet("/", (ILogger<Program> logger) =>
14{
15    logger.LogDebug("Debug message");       // Only in Development
16    logger.LogInformation("Info message");  // Development + Staging
17    logger.LogWarning("Warning message");   // All environments
18    logger.LogError("Error message");       // All environments
19    return "OK";
20});

ILogger is the recommended approach for .NET applications. Log levels are configured per environment without recompilation.

Common Pitfalls

  • Expecting Debug.WriteLine in production logs: Debug.WriteLine produces no output in Release builds — not even in the Output window. If you need logging in production, use Trace, ILogger, or a logging framework like Serilog.
  • Side effects in Debug calls: Debug.WriteLine(ExpensiveComputation()) — the entire expression including ExpensiveComputation() is removed in Release. If the method has side effects your code depends on, those effects disappear in Release builds.
  • Assuming Trace is always active: Trace requires the TRACE preprocessor symbol. While it is defined by default in both Debug and Release configurations, it can be removed. Verify your .csproj defines TRACE in Release.
  • String formatting overhead: Debug.WriteLine($"Processing {JsonSerializer.Serialize(largeObject)}") — in Debug builds, the string is constructed even though the output may go nowhere (no listener attached). Use Debug.WriteLineIf or check Debugger.IsAttached for expensive formatting.
  • Confusing #if DEBUG with [Conditional("DEBUG")]: #if DEBUG removes the code block at compile time. [Conditional("DEBUG")] removes the call sites but keeps the method definition. With [Conditional], the method exists in the assembly but is never called.

Summary

  • Debug.WriteLine is completely removed from Release builds — zero overhead
  • The [Conditional("DEBUG")] attribute causes the compiler to strip all Debug.* calls when DEBUG is not defined
  • Use Trace.WriteLine for logging that persists in Release builds
  • Use ILogger (Microsoft.Extensions.Logging) for configurable, environment-aware logging
  • Never rely on side effects inside Debug.* calls — they are removed in Release
  • #if DEBUG removes code blocks; [Conditional("DEBUG")] removes call sites

Course illustration
Course illustration

All Rights Reserved.