Managing Errors, Warnings, and Configurations in C# and .NET

Managing Errors, Warnings, and Configurations in C# and .NET

When we activated static code analysis for the first time in one of my last projects, the overwhelming number of warnings exceeded expectations and highlighted gaps in the code. Without making any changes, the project already had a significant number of warnings. After activating additional analyzers and updating some configurations, this number temporarily increased dramatically.

The high number of warnings was initially daunting, but we saw it as an opportunity to significantly improve our code quality. At first glance, it seemed easier to suppress or ignore these warnings. But as I often remind my team, “The code you create is a valuable legacy, so it’s important to build it carefully.” Ignoring warnings today creates obstacles for future developers—and that could very well include you six months down the line.

This experience reinforced the importance of managing warnings and errors systematically. Let me share some of the lessons we learned, the strategies we used to tame those 60,000 warnings, and how you can apply these techniques to your own projects.

From Chaos to Clarity: Why Warnings Matter

The Cost of Ignoring Warnings

Warnings signal potential issues, alerting us to things that might go wrong. Ignoring these warnings can lead to subtle bugs, poor maintainability, and wasted time during debugging. When a project accumulates thousands of warnings, it creates warning fatigue: developers become so desensitized to them that even critical issues go unnoticed.

Our project’s warnings could be grouped into three categories:

  1. Legacy Code Issues: Deprecated APIs and outdated practices from years of development.
  2. Analyzer Rules: New code-quality rules introduced by Roslyn analyzers and other tools.
  3. Nullability Warnings: Warnings about potential null reference exceptions after enabling nullable reference types.

Each required a distinct approach to address.

Configuring .NET Build: Turning the Tide Against Warnings

The first step in tackling warnings is understanding how to configure their behavior in .NET Build. By setting global and file-specific properties, we gained control over how warnings were treated across the project.

Global Properties in .NET Build

A centralized configuration helps ensure consistency across your solution. While some properties tighten the rules around warnings, others allow for flexibility where needed. Here’s how we set up critical properties in our project:

<PropertyGroup>
    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
    <WarningsAsErrors>CS8602;CS8604</WarningsAsErrors> <!-- Specific warnings treated as errors -->
    <WarningsNotAsErrors>CS1591</WarningsNotAsErrors> <!-- Exceptions for specific warnings -->
    <NoWarn>CS0618</NoWarn> <!-- Suppressing non-critical warnings -->
</PropertyGroup>
  • TreatWarningsAsErrors: This global setting enforces a “no warnings allowed” policy, treating every warning as a build-breaking error. While this is great for enforcing high standards, it can be overly strict for legacy codebases.
  • WarningsAsErrors: This allows you to escalate specific warnings to errors. For example, warnings like CS8602 (dereference of a possibly null reference) and CS8604 (null passed as a non-nullable parameter) were prioritized as errors in our project.
  • WarningsNotAsErrors: A complementary property to WarningsAsErrors, it provides exceptions to the rule. In our case, we decided not to escalate CS1591 (missing XML documentation) to an error because enforcing this across the entire project wasn’t immediately feasible.
  • NoWarn: Temporarily suppresses warnings that are acknowledged but cannot be resolved right away. For instance, CS0618 (usage of deprecated APIs) was suppressed for legacy code that we plan to refactor incrementally.

Combining these properties allowed us to enforce critical standards while giving flexibility for legacy code.

Practical Strategies for Managing Warnings

1. Triage and Categorize Warnings

Not all warnings are created equal. We divided them into:

  • Critical Warnings: Must be resolved immediately (e.g., potential null reference exceptions).
  • Informational Warnings: Desirable to fix but not urgent (e.g., missing XML documentation comments).
  • Legacy Warnings: Related to outdated APIs or practices that require phased modernization.

Example: Prioritizing Critical Warnings

Critical warnings, like nullability issues, were escalated to errors using the WarningsAsErrors property:

<WarningsAsErrors>CS8602;CS8604</WarningsAsErrors>

This ensured they were always addressed before a build could succeed.

2. Using Automatic Code Fixers Wisely

Visual Studio provides a convenient feature for resolving many warnings through automatic code fixers. These tools analyze the code and offer one-click solutions for issues, such as simplifying expressions, adding missing null checks, or suppressing warnings with #pragma directives. While these fixers can save time, they must be used with caution.

Example: Applying an Automatic Code Fix

Consider the following nullable warning:

string? name = null;
Console.WriteLine(name.Length); // Warning: Possible null reference exception

Visual Studio might suggest adding a null-forgiving operator (!) to suppress the warning:

Console.WriteLine(name!.Length); // Suppression applied

While this eliminates the warning, it introduces a potential runtime exception if name is actually null. This type of fix addresses the symptom but not the root cause, leaving the code vulnerable.

Risks of Overusing Automatic Fixers

  • Masking Real Issues: Automatic fixes often silence warnings without addressing underlying logic problems.
  • Introducing Complexity: Generated fixes can add unnecessary code, such as redundant null checks, making the code harder to read and maintain.
  • False Sense of Security: Developers might trust that the issue is resolved, only to find that the automatic fix created new problems.

Best Practices for Using Code Fixers

  1. Review Every Fix: Treat automatic suggestions as starting points. Always evaluate whether the proposed fix aligns with your code’s intent.
  2. Combine with Analysis: Use code fixers in tandem with a clear understanding of the warning.
  3. Avoid Blanket Suppressions: If a fixer suggests suppressing a warning (e.g., adding #pragma warning disable), consider whether this is appropriate or just hiding a deeper issue.

By using automatic code fixers wisely, you can ensure that they improve your code’s quality rather than creating hidden risks.

Real-World Example: Integrating Warning Management in CI/CD Pipelines

One of the most effective ways we managed warnings was by integrating warning handling into our CI/CD pipeline. This allowed us to enforce consistent rules across every build and ensure that no warning could slip through the cracks during deployment.

Automated Build Configuration

We configured our CI pipeline to treat warnings as errors, particularly for release builds. This configuration forced the team to resolve any warnings before code could be deployed, ensuring that only clean code made it to production. By doing this, we effectively ensured that our codebase maintained a high standard without relying solely on manual intervention.

Here’s how we configured the pipeline using a YAML file for a .NET Core project to treat warnings as errors during the build process:

steps:
  - task: DotNetCoreCLI@2
    inputs:
      command: 'build'
      arguments: '--configuration Release /p:TreatWarningsAsErrors=true'

In this setup:

  • For release builds, the TreatWarningsAsErrors=true argument was specified, ensuring that the build would fail if any warning appeared.
  • For debug builds, we chose to allow warnings, as they would not disrupt the ongoing development work but would still be tracked for later resolution.

Ensuring Consistency Across Environments

By enforcing these settings in the pipeline, we ensured that no matter who worked on the code, whether locally or remotely, the same strict rules were applied. This helped prevent situations where developers ignored warnings during their local builds but let them accumulate over time, only to be caught late in the development cycle.

Continuous Monitoring and Refinement

As part of our ongoing integration process, we continually refined the warning rules based on feedback and evolving project needs. We also configured the pipeline to provide detailed reports on warnings and errors, which could be easily reviewed by the team. This helped us identify patterns or areas that required more attention, such as recurring issues with nullability or outdated API usage.

By integrating warning management into our CI/CD pipeline, we automated and enforced quality standards across the board. This shift not only improved the code’s stability but also created a more accountable and transparent development process.

Final Thoughts: Building a Legacy

As software developers, the code we write today becomes the foundation for future teams—or for ourselves. Ignoring warnings and errors undermines that foundation. By managing them effectively, we leave behind a valuable legacy of maintainable, high-quality code.

Through the measures we implemented, including integrating warning management into our CI/CD pipeline, we were able to address a number of previously unknown issues. Many bugs that had quietly lurked in the codebase were brought to light and resolved—issues that had been hidden under the surface and hadn’t surfaced until we made the handling of warnings and errors a priority. Some of these bugs were revealed through the warnings themselves, while others came to light as we reviewed log files during builds and deployments.

This process reinforced a crucial point: warnings are not just noise. They often signal deeper issues that need to be resolved before they cause significant problems down the road.

While we may never completely rid our projects of warnings, the key is to manage them effectively—and in doing so, create cleaner, more maintainable code that will stand the test of time.

Comments

VG Wort