← Back to all articles

A Practical Guide to WarningsAsErrors

Published

Software warnings feel harmless until the day one of them turns into a late-night outage.

In this article, we’ll walk through turning those yellow squiggles into early-failing, production-saving errors using the WarningsAsErrors family of settings.

You’ll see exactly how to flip the switch, when to fine-tune it, and what to do if your project is already drowning in warnings.

Why a Single Warning Can Sink a Release

A typical C# solution compiles even when dozens of diagnostics flash by because the default severity for most compiler diagnostics and analyzer rules is warning. Teams get used to the noise, and real defects hide in plain sight:

  • Nullability warnings such as CS8602 that later crash the app.

  • Unobserved task exceptions masked by ignored async warnings.

  • API misuse caught by analyzers but never fixed.

Put bluntly, a warning is a bug you’ve scheduled for production.

Code Example

public static class Program
{
    static void Main(string[] args)
    {
        // CS0168: Variable declared but never used
        int unusedVariable;

        // CS8600: Converting null literal or possible
        // null value to non-nullable type
        string nullableStr = GetNullableString();

        // CS8602: Dereference of a possibly null reference
        Console.WriteLine($"Length: {nullableStr.Length}");
    }

    // Method that can return null
    private static string? GetNullableString()
    {
        return DateTime.Now.Second % 2 == 0 ? "Test string" : null;
    }
}

The Error List reports CS0168, CS8600, and CS8602, yet the build still finishes successfully with zero errors and three warnings.

image

TreatWarningsAsErrors

Adding one line to your .csproj turns every compiler warning into an error:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  </PropertyGroup>
</Project>

From now on, the build fails fast.

image

Pros

  1. Zero chance of shipping fresh compiler warnings.

  2. A clear signal for every developer.

Cons

  1. It can be brutal when a legacy codebase already has hundreds of diagnostics.

WarningsAsErrors

If you need more control, list only the warning codes that should be treated as errors:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <WarningsAsErrors>CS8600</WarningsAsErrors>
  </PropertyGroup>
</Project>

All other warnings remain warnings and won’t break the build. To include multiple warning codes, separate them with semicolons:

<WarningsAsErrors>CS8600;CS8602</WarningsAsErrors>

image

WarningsNotAsErrors

Sometimes you want the inverse: keep most warnings as errors except for a few noisy ones.

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <WarningsNotAsErrors>CS0168</WarningsNotAsErrors>
  </PropertyGroup>
</Project>

image

If you already have hundreds of warnings, this approach might not be enough. In one of the next posts, I’ll share tactics for cleaning them up quickly.

Key Takeaways

  1. A warning ignored today can become tomorrow’s post-mortem.

  2. TreatWarningsAsErrors is the simplest and most reliable safety net.

  3. Selective lists with WarningsAsErrors and WarningsNotAsErrors let you adopt the rule gradually.

  4. Automate compiler switches, analyzers, and formatting to keep the signal clean.

Lock warnings down now, and let your production environment sleep easier later.