Skip to content
Search lessons, topics, tests…
Esc

    ↑ ↓ moveEnter openEsc close

    Module 1 · 1. Foundations, Tooling, and Your First C# Programs · Lesson 2 of 24

    Reading Compiler Diagnostics and Building with Confidence

    Learning outcomes

    By the end of this lesson, you can classify compiler errors, warnings, and runtime failures; use a repeatable build-debug-fix loop; and explain why warnings belong in engineering work. You will build three independently broken examples, compare each with a corrected version, and check a complete port-validation program using normal and invalid inputs.

    First-pass route

    Work through the syntax, type, and nullable examples one pair at a time. Keep each broken example in its own project; build it, read its diagnostic, then compare the corrected version. The deliberately broken projects are part of the exercise, so a clean build of everything together is not the goal.

    Focus first on reading diagnostics and making one focused repair, including the difference between changing warning policy and adding a real guard. The complete port-validation program is an optional deeper lab on a first pass: notice that it can build successfully and still reject input when it runs, then return to its full implementation and boundary-case checks after the three diagnostic examples.

    Core ideas

    The compiler protects language and type-system rules before the program runs. Start with the first diagnostic because later messages may be consequences of one missing token or incorrect type. Read the file and location, identify the diagnostic code and severity, make one focused repair, then rebuild. The first reported location is a useful starting point, not a guarantee that every later message has the same cause.

    A warning is not proof that the program is broken, but it is evidence worth resolving. Nullable-reference warnings, unreachable code, and unobserved async work often predict production defects. A successful build does not prove that inputs satisfy the application's rules or that every runtime path is safe.

    Debug and Release are build configurations, not quality levels. Debug favors interactive diagnosis; Release enables optimizations in the normal SDK configuration and is the configuration you should validate before deployment. Project settings can change the defaults. The examples below compare builds in Debug and Release; the listed process-output checks run the Release assemblies.

    Exact project and file setup

    Prerequisite: an installed .NET 10 SDK, its net10.0 targeting pack, and the .NET 10 runtime. Run dotnet --version and record the actual SDK patch. This lesson targets net10.0 and explicitly selects C# 14.0; it does not install or download an SDK. If the SDK or local targeting/runtime packs are missing, stop and have the environment owner provision them before continuing.

    Create a new directory named CompilerConfidence. Keep it outside another project's directory and outside a directory governed by an unrelated global.json, Directory.Build.props, or Directory.Build.targets. Open your terminal in CompilerConfidence. Create this layout manually in your editor; no template command or package installation is needed.

    Text
    CompilerConfidence/
      NuGet.Config
      SyntaxBroken/     SyntaxBroken.csproj, Program.cs
      SyntaxFixed/      SyntaxFixed.csproj, Program.cs
      TypeBroken/       TypeBroken.csproj, Program.cs
      TypeFixed/        TypeFixed.csproj, Program.cs
      NullableWarning/  NullableWarning.csproj, Program.cs
      NullableFixed/    NullableFixed.csproj, Program.cs
      PortValidation/   PortValidation.csproj, Program.cs

    Each slash-named directory above contains two separate files. For example, SyntaxBroken/SyntaxBroken.csproj and SyntaxBroken/Program.cs are separate files. Do not paste all the programs into one project: the broken examples must remain isolated so one failure cannot hide another.

    Create NuGet.Config in the root with this complete content:

    XML
    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
      <packageSources>
        <clear />
      </packageSources>
    </configuration>

    Create all seven .csproj files with the following identical content. Only the filename and containing directory change:

    XML
    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net10.0</TargetFramework>
        <LangVersion>14.0</LangVersion>
        <ImplicitUsings>disable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
        <NuGetAudit>false</NuGetAudit>
      </PropertyGroup>
    </Project>

    These projects contain no PackageReference entries. The feed list is empty and package auditing is disabled, so restore is for SDK-provided framework assets, not external packages. Keep implicit usings disabled as shown; every source below declares its own imports. Nullable analysis is enabled and warnings are not initially treated as errors.

    For each project, run restore once before the build commands in its section. For example:

    Text
    dotnet --version
    dotnet restore SyntaxBroken/SyntaxBroken.csproj --configfile NuGet.Config --verbosity quiet -p:NuGetAudit=false
    dotnet build SyntaxBroken/SyntaxBroken.csproj --no-restore --configuration Debug --verbosity minimal --nologo -t:Rebuild

    Replace both occurrences of SyntaxBroken with the chosen project name. --no-restore skips implicit restore; -t:Rebuild selects the rebuild target. Microsoft build reference Restore should exit 0 when the prerequisite is satisfied. -t:Rebuild requests compilation again, so an unchanged file does not hide the diagnostic behind an up-to-date build result. Capture the build's exit status immediately: $LASTEXITCODE in PowerShell, or $? in a POSIX shell.

    The expected compiler results below are a contract to check, not a claim that your local toolchain has already produced them. Match diagnostic code and severity. English message examples help you read the output; wording, full paths, line/column positions, summary duplication, and timings are not exact-match requirements. A command reporting missing SDKs, assets, or reference packs has not yet tested the intended C# problem.

    Syntax error and focused correction

    CS1002 identifies a missing semicolon. Here, the WriteLine statement has no terminator. Microsoft CS1002 reference

    Create SyntaxBroken/Program.cs:

    C#
    using System;
    
    Console.WriteLine("Ready")

    After its restore, build it in both configurations:

    Text
    dotnet build SyntaxBroken/SyntaxBroken.csproj --no-restore --configuration Debug --verbosity minimal --nologo -t:Rebuild
    dotnet build SyntaxBroken/SyntaxBroken.csproj --no-restore --configuration Release --verbosity minimal --nologo -t:Rebuild

    Expected: each build exits 1, with unique diagnostic error CS1002. An illustrative message is ; expected. Inspect line 3 and add the missing punctuation rather than trying to run the failed build. Compiler output can repeat a diagnostic in its final summary; that is not a second independent problem.

    Create the corrected counterpart, SyntaxFixed/Program.cs:

    C#
    using System;
    
    Console.WriteLine("Ready");
    Text
    dotnet restore SyntaxFixed/SyntaxFixed.csproj --configfile NuGet.Config --verbosity quiet -p:NuGetAudit=false
    dotnet build SyntaxFixed/SyntaxFixed.csproj --no-restore --configuration Release --verbosity minimal --nologo -t:Rebuild
    dotnet SyntaxFixed/bin/Release/net10.0/SyntaxFixed.dll

    Expected build: exit 0, no warnings or errors. Expected application: stdout Ready followed by a newline; stderr empty; exit 0. Repeat the build with --configuration Debug to check that configuration as well.

    Type error and focused correction

    CS0029 reports an assignment whose types cannot be implicitly converted. Our string literal is not an integer value. Microsoft CS0029 reference

    Create TypeBroken/Program.cs:

    C#
    using System;
    
    int port = "8080";
    Console.WriteLine($"Listening on {port}");
    Text
    dotnet restore TypeBroken/TypeBroken.csproj --configfile NuGet.Config --verbosity quiet -p:NuGetAudit=false
    dotnet build TypeBroken/TypeBroken.csproj --no-restore --configuration Debug --verbosity minimal --nologo -t:Rebuild
    dotnet build TypeBroken/TypeBroken.csproj --no-restore --configuration Release --verbosity minimal --nologo -t:Rebuild

    Expected: both builds exit 1, with unique diagnostic error CS0029. The English message commonly reads Cannot implicitly convert type 'string' to 'int'. Look at the declared type and the quoted value on line 3; the statement already has its semicolon.

    Create TypeFixed/Program.cs:

    C#
    using System;
    
    int port = 8080;
    Console.WriteLine($"Listening on {port}");
    Text
    dotnet restore TypeFixed/TypeFixed.csproj --configfile NuGet.Config --verbosity quiet -p:NuGetAudit=false
    dotnet build TypeFixed/TypeFixed.csproj --no-restore --configuration Release --verbosity minimal --nologo -t:Rebuild
    dotnet TypeFixed/bin/Release/net10.0/TypeFixed.dll

    Expected build: exit 0, no warnings or errors. Expected application: stdout Listening on 8080 and a newline; stderr empty; exit 0. Also rebuild in Debug. This repair uses a numeric constant because the example's value is known in the source. The port program later deals with user-supplied text through validation instead of pretending it is already a number.

    Nullable warning and a real guard

    CS8602 flags a member access on a potentially null reference. A null check can establish a safe path before dereferencing. Microsoft nullable warnings reference

    Create NullableWarning/Program.cs:

    C#
    using System;
    
    string? host = args.Length == 0 ? null : args[0];
    Console.WriteLine($"Host length: {host.Length}");
    Text
    dotnet restore NullableWarning/NullableWarning.csproj --configfile NuGet.Config --verbosity quiet -p:NuGetAudit=false
    dotnet build NullableWarning/NullableWarning.csproj --no-restore --configuration Debug --verbosity minimal --nologo -t:Rebuild
    dotnet build NullableWarning/NullableWarning.csproj --no-restore --configuration Release --verbosity minimal --nologo -t:Rebuild

    Expected: each ordinary build exits 0 with unique diagnostic warning CS8602, near host.Length on line 4. An illustrative English message is Dereference of a possibly null reference. Do not execute this deliberately unsafe fixture. When there is no argument, its own assignment gives host the value null, yet the next statement attempts to read a member from it.

    Now rebuild the same source with a stricter policy for this invocation:

    Text
    dotnet build NullableWarning/NullableWarning.csproj --no-restore --configuration Release --verbosity minimal --nologo -t:Rebuild -p:TreatWarningsAsErrors=true

    Expected: exit 1 with unique diagnostic error CS8602. The source has not changed; the command changed the build's acceptance policy. This does not insert a runtime guard or repair the code. The property in the project file remains false, and a later ordinary build uses that file setting again. Never use an old output assembly as proof that a failed build succeeded.

    Create the corrected counterpart, NullableFixed/Program.cs:

    C#
    using System;
    
    string? host = args.Length == 0 ? null : args[0];
    if (host is null)
    {
        Console.Error.WriteLine("Provide a host name.");
        return 2;
    }
    
    Console.WriteLine($"Host length: {host.Length}");
    return 0;
    Text
    dotnet restore NullableFixed/NullableFixed.csproj --configfile NuGet.Config --verbosity quiet -p:NuGetAudit=false
    dotnet build NullableFixed/NullableFixed.csproj --no-restore --configuration Release --verbosity minimal --nologo -t:Rebuild
    dotnet NullableFixed/bin/Release/net10.0/NullableFixed.dll api
    dotnet NullableFixed/bin/Release/net10.0/NullableFixed.dll

    Expected build: exit 0, no warnings or errors; repeat in Debug. With api, stdout is Host length: 3 plus newline, stderr is empty, and exit is 0. With no argument, stdout is empty, stderr is Provide a host name. plus newline, and exit is 2. The early return keeps that path away from the member access. An explicitly supplied empty string is different from an absent argument: it follows the non-null path, prints Host length: 0, and exits 0. This small example is about null handling, not hostname validation.

    Optional deeper lab: complete port validation program

    The earlier examples concern what the compiler can establish from source. The next program compiles cleanly but still rejects invalid input at runtime. Keep the lesson's ParsePort method and its ArgumentOutOfRangeException contract; add a complete command-line boundary with predictable output and exit status.

    Create PortValidation/Program.cs:

    C#
    using System;
    using System.Globalization;
    
    static int ParsePort(string? raw)
    {
        if (!int.TryParse(raw, NumberStyles.Integer,
                CultureInfo.InvariantCulture, out var port)
            || port is < 1 or > 65535)
        {
            throw new ArgumentOutOfRangeException(
                nameof(raw), "Port must be 1-65535.");
        }
    
        return port;
    }
    
    if (args.Length != 1)
    {
        Console.Error.WriteLine("Usage: PortValidation <port>");
        return 2;
    }
    
    try
    {
        int port = ParsePort(args[0]);
        Console.WriteLine($"Listening on {port.ToString(CultureInfo.InvariantCulture)}");
        return 0;
    }
    catch (ArgumentOutOfRangeException)
    {
        Console.Error.WriteLine("Port must be 1-65535.");
        return 2;
    }

    ParsePort returns an integer from 1 through 65535, inclusive. If parsing fails or the integer is outside that range, the method throws ArgumentOutOfRangeException. The command-line boundary catches that exception and reports one fixed message. It deliberately avoids ex.Message, whose extra parameter details and localized text are a poor machine-output contract.

    The application requires exactly one argument. Missing or extra arguments are a usage problem. Other rejected inputs use the port-validation message. All failures here use stderr and return 2; success uses stdout and returns 0. The literal Listening on output is demonstration text only. The program validates and prints the port; it does not bind a socket, start a server, or prove that a port listener exists.

    Text
    dotnet restore PortValidation/PortValidation.csproj --configfile NuGet.Config --verbosity quiet -p:NuGetAudit=false
    dotnet build PortValidation/PortValidation.csproj --no-restore --configuration Debug --verbosity minimal --nologo -t:Rebuild
    dotnet build PortValidation/PortValidation.csproj --no-restore --configuration Release --verbosity minimal --nologo -t:Rebuild
    dotnet PortValidation/bin/Release/net10.0/PortValidation.dll 8080
    dotnet PortValidation/bin/Release/net10.0/PortValidation.dll 0
    dotnet PortValidation/bin/Release/net10.0/PortValidation.dll abc
    dotnet PortValidation/bin/Release/net10.0/PortValidation.dll

    Both builds should exit 0 without compiler diagnostics. Check the process output and status after each application command, before running the next:

    • 8080: stdout Listening on 8080 plus newline; empty stderr; exit 0
    • 1: stdout Listening on 1 plus newline; empty stderr; exit 0
    • 65535: stdout Listening on 65535 plus newline; empty stderr; exit 0
    • 0, 65536, -1, abc, 2147483648, an empty string, or 80.5: empty stdout; stderr Port must be 1-65535. plus newline; exit 2
    • No arguments, or two arguments such as 8080 9090: empty stdout; stderr Usage: PortValidation <port> plus newline; exit 2
    • One argument whose value is a leading space, +8080, and a trailing space: stdout Listening on 8080 plus newline; empty stderr; exit 0

    The explicit invariant integer parser allows a leading sign and surrounding whitespace; it does not accept fractional input. The large numeric string is outside the integer parser's capacity, so the same validation branch handles it. These accepted and rejected inputs define this exercise's contract. A stricter digits-only policy would be a deliberate product change, not a compiler fix. Use an argument-list runner for the empty-string and whitespace cases so shell quoting cannot silently alter your test.

    A repeatable build debug fix loop

    1. Build the specific project and configuration. Save the exit status and diagnostics before another command overwrites them.
    1. Start with the first diagnostic. Separate its location, code, severity, and message. Check the surrounding source rather than treating a line number as an infallible repair instruction.
    1. Predict a small correction and rebuild. Keep syntax, type, nullable, and input-validation experiments in separate projects.
    1. When the build succeeds, execute the corrected assembly with both accepted and rejected inputs. Check stdout, stderr, and exit status independently.
    1. Repeat the relevant build and behavior checks in Release before deployment. This lab does not configure trimming or deployment packaging, so its Release checks do not test those additional choices.

    Failure modes

    • Fixing symptoms from the bottom of the diagnostic list instead of investigating the first root error
    • Suppressing nullable warnings without proving the value cannot be null
    • Testing only Debug builds and discovering configuration-sensitive behavior during deployment
    • Counting a warnings-as-errors failure as a repaired nullable path
    • Running a stale assembly after a failed build or mixing build log lines into the application's expected output

    Practice with worked answers

    First predict each result without running the command. Then compare your environment's results with the expected contract.

    1. Introduce one syntax error by removing the semicolon in the fixed syntax project. Which stage catches it? Compilation fails with CS1002. Restore the semicolon and rebuild.
    1. Replace the integer literal in the fixed type project with "8080". Which stage catches it? Compilation fails with CS0029. Restore the integer literal.
    1. Remove the null guard from the corrected host program. Which stage reports the unsafe member access? Compilation reports CS8602. The ordinary policy permits a build; the strict policy rejects it. Restore the guard rather than suppressing the report.
    1. Pass 0 to the port program. Does it create a compiler diagnostic? No. Its already-compiled validation method rejects the value; the boundary emits the fixed error on stderr and exits 2.
    1. After all repairs, rebuild SyntaxFixed, TypeFixed, NullableFixed, and PortValidation in Release. Each should have no compiler diagnostics. Check the success and rejection paths above; a clean compiler result alone is not the acceptance test.

    The supplied broken projects intentionally remain broken as teaching fixtures. Do not try to obtain a clean aggregate build containing them. The clean Release goal applies to the corrected projects and the port program.

    Interview check with a model answer

    Explain the difference between a compile-time error, a runtime exception, and a failed business validation. Give an example of each.

    A compile-time error prevents a successful build: assigning our quoted port literal to an integer is one example. A runtime exception is an event during execution: ParsePort throws when it rejects an input. Failed validation is the application deciding that data violates its contract: port 0 violates the allowed range. In this program, validation uses an exception internally, and the command-line boundary converts it to a controlled error message and exit 2. These categories describe different aspects of the same flow, so validation failure and a runtime exception are not always separate events.

    Verification checklist

    • Record the SDK patch and target framework
    • Observe the expected compiler code and severity for each independent broken example
    • Confirm that the strict nullable build changes severity and exit status without changing source
    • Rebuild every corrected project in Debug and Release
    • Run the corrected programs and port boundary cases, checking separate output streams and exits
    • Keep actual verification results separate from these expected results

    Analogy

    Everyday picture

    Imagine a ticket desk whose instructions are reviewed before it opens. An unfinished instruction or a word in a number-only box blocks preparation. A step that assumes every visitor brings an optional card is flagged as risky. After opening, the desk still checks each request.

    Mapping. The first two problems model syntax and type errors. The card warning models unsafe null access. Requiring a flag-free sheet changes the acceptance policy; adding a missing-card check changes the instructions.

    Where it stops. Human reviewers can guess intent; compiler diagnostics follow language rules. Passing the sheet review does not guarantee that each request succeeds. Read the actual port-validation program to see how it rejects input and handles the resulting exception.

    Cheat sheet (PDF)

    csharp-compiler-diagnostics-build-confidence-companion.pdf16 pages · 92 KB
    Every page, in this page.

    Practice

    Sign in to mark lessons done and keep your place in the course.Sign in