Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C# & .NET (C# 14, .NET 10 LTS)  Module 1 – Start here: C#, .NET and your first programs

Compiler errors, stack traces and debugging

Read a C# compiler error by its file, line and id, fix a build in rounds, trace an exception to the failing line, and find a wrong value with a debugger.

  • Beginner
  • 20 minutes
  • Examples run with .NET 10.0.401 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Decode a compiler diagnostic by its file, line, column, severity, id and message
  • Fix a failing build in rounds from its first error, and make warnings fail the build
  • Read an unhandled exception's stack trace down to the first line of your own code
  • Explain why a stack overflow and a failed Debug.Assert end the process, and when an assertion runs at all
  • Use a breakpoint, stepping and a watch in a debugger to find a wrong value

Before you start

What you’ll learn · Before you start

What you will learn

  • Decode a compiler diagnostic by its file, line, column, severity, id and message
  • Fix a failing build in rounds from its first error, and make warnings fail the build
  • Read an unhandled exception's stack trace down to the first line of your own code
  • Explain why a stack overflow and a failed Debug.Assert end the process, and when an assertion runs at all
  • Use a breakpoint, stepping and a watch in a debugger to find a wrong value

Before you start

On this page

The first build of a nine-line C# program with three mistakes in it reported one error, not three. Every problem in C# reaches you as text like that: a diagnostic: A message from a build tool about your code: its file, line and column, a severity (error or warning), an id such as CS1002 and a text. An error stops the build; a warning does not. from the compiler while you build, or a stack trace: The list of method calls that were running when an exception was thrown, newest first, each with its file and line when debug symbols are available. An unhandled exception prints it to standard error. from the runtime when the program fails while it runs.

This lesson takes both apart, fixes a build in rounds, follows an exception from the line that threw it back to the start of the program, shows two failures that end a program before any catch can run, and finds a wrong value with a debugger instead of guessing.

A diagnostic says where, how bad and what

This program adds up the prices on a receipt. It has three mistakes, of the kind everybody types:

round-1/Program.cs C# · round-1/Program.cs
// Adds up the prices on a receipt. It has three mistakes.
int[] prices = [120, 80, 45];
int total = 0;
foreach (int price in prices)
{
    total += price
}
string count = prices.Length;
Console.WriteLine($"{count} items, total {totl}");

Most folders of this lesson’s examples hold a small console project, like the ones of the previous lesson. The session builds this one, then two corrected copies of it, and runs the last; -v q (quiet) keeps the build’s own progress lines out of the way:

Three rounds of the receipt program Python · errors_session.py
from session import run

# The receipt program built three times: with its three mistakes, after the first fix, and after all three, which
# then runs. -v q (quiet) leaves out the restore and progress lines, so only the diagnostics and the summary are
# printed.
run("""
cd round-1
!dotnet build -v q
cd ../round-2
!dotnet build -v q
cd ../round-3
dotnet build -v q
dotnet run
""")

Output

$ cd round-1
$ dotnet build -v q
round-1/Program.cs(6,19): error CS1002: ; expected [round-1/Receipt.csproj]

Build FAILED.

round-1/Program.cs(6,19): error CS1002: ; expected [round-1/Receipt.csproj]
    0 Warning(s)
    1 Error(s)

Time Elapsed …
[exit status 1]
$ cd ../round-2
$ dotnet build -v q
round-2/Program.cs(8,16): error CS0029: Cannot implicitly convert type 'int' to 'string' [round-2/Receipt.csproj]
round-2/Program.cs(9,43): error CS0103: The name 'totl' does not exist in the current context [round-2/Receipt.csproj]

Build FAILED.

round-2/Program.cs(8,16): error CS0029: Cannot implicitly convert type 'int' to 'string' [round-2/Receipt.csproj]
round-2/Program.cs(9,43): error CS0103: The name 'totl' does not exist in the current context [round-2/Receipt.csproj]
    0 Warning(s)
    2 Error(s)

Time Elapsed …
[exit status 1]
$ cd ../round-3
$ dotnet build -v q

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed …
$ dotnet run
3 items, total 245

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 errors_session.py

The first build printed one line of interest, twice: once as it happened and once in the summary under Build FAILED. Every diagnostic has the same parts. Take the first one apart:

Part In the first diagnostic What it tells you
File round-1/Program.cs the file to open
Line and column (6,19) line 6, character 19: just after price
Severity error an error stops the build; a warning does not
Id CS1002 the code to look up in the documentation
Message ; expected what the compiler expected and did not find
Project [round-1/Receipt.csproj] the project that was being built

The letters of the id say which tool is talking, which tells you where to look for the cause:

Id starts with Reported by Seen in this track
CS the C# compiler CS1002 here; CS8602, a warning about null, later in this lesson
CA, IDE the code analyzers that come with the SDK: quality and style rules CA2266 in the lesson on file-based app directives
NU NuGet, while it restores packages NU1301 when restore could not reach nuget.org
NETSDK the .NET SDK’s own build steps NETSDK1022 for a file listed twice, in the previous lesson
MSB MSBuild, the build engine problems with the build itself rather than your code

Checkpoint

Check: Reading a diagnostic

  1. Question 1 of 2 The second build printed round-2/Program.cs(9,43): error CS0103: The name 'totl' does not exist in the current context. On which line of Program.cs is the mistake?

    Type a number.

    A hint

    The two numbers in brackets are the line, then the column.

    Show the answer: Question 1 of 2

    Answer: 9 line

    In (9,43) the first number is the line and the second the column, so the mistake is on line 9, at character 43, where totl starts inside the interpolated string.

  2. Question 2 of 2 A build stops with error NU1301 before compiling anything. Which part of the build reported it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: NuGet, while it restored packages

    The letters of an id say which tool is talking: CS the compiler, CA and IDE the analyzers, NU NuGet, NETSDK the SDK's build steps, MSB MSBuild. NU1301 means that restore could not load a package source.

Fix the first error, then build again

Round 1 reported CS1002 alone, yet two more mistakes were in the file all along. Once the semicolon was back, the second build got past the syntax, checked what the code means and found them: CS0029 on line 8, because an int cannot be stored in a string without a conversion, and CS0103 on line 9, because no name totl exists. The third build, with int count and total, reported 0 errors, and dotnet run printed 3 items, total 245. Step through the three rounds:

Interactive · stage by stage Fixing a build round by round Step through the three builds of the receipt program. The arrow aside is the way back when a build still fails.
Fixing a build round by round, step 6 of 6. Showing Build, Read the first error (CS0029 and CS0103), Fix that one cause, Build again (0 errors), Run the program (3 items, total 245). In focus: Run the program.errors leftBuilddotnet buildRead the first errorCS0029 and CS0103Fix that one causeand only that oneBuild again0 errorsRun the program3 items, total 245dotnet run

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Round 1. The first build of the receipt program reports one error, CS1002 at line 6, column 19: a semicolon is missing after total += price. The other two mistakes are not reported yet.
  2. Read the first one. Start with the first diagnostic in the list. Here it is the only one; in a longer list, later errors are often consequences of the first, so they may vanish with it.
  3. Fix its cause. Add the semicolon at the end of line 6 and change nothing else, so that the next build tells you whether this fix was the right one.
  4. Round 2. The second build gets past the syntax and checks what the code means. Two errors appear that were there all along: CS0029 on line 8 and CS0103 on line 9.
  5. Back to the first error. CS0029: an int cannot be stored in a string, so count becomes an int. CS0103: no name totl exists; it is a typing slip for total. Both fixes go in, then one more build.
  6. Round 3. The third build reports 0 warnings and 0 errors, and dotnet run prints 3 items, total 245.

Two habits follow from those runs. Start each round at the first diagnostic in the list, because a later error is often only a consequence of an earlier one and goes away with it. And build again after each fix: a fixed syntax error can let mistakes of meaning appear that were hidden behind it.

Key idea

Fix the first diagnostic and build again: later errors may be its consequences, and mistakes of meaning can wait behind a mistake of syntax until it is fixed.

The position in a diagnostic is where the compiler noticed the problem, which is not always where you made the mistake. This file’s loop has lost its closing brace:

brace/Program.cs C# · brace/Program.cs
// The closing brace of the foreach loop is missing: it belongs after line 11.
Console.WriteLine(Receipt.Total([120, 80, 45]));

static class Receipt
{
    public static int Total(int[] prices)
    {
        int total = 0;
        foreach (int price in prices)
        {
            total += price;
        return total;
    }

    public static int Count(int[] prices) => prices.Length;
}
Building brace Python · brace_session.py
from session import run

# brace, a program whose foreach loop has lost its closing brace, built once.
run("""
cd brace
!dotnet build -v q
""")

Output

$ cd brace
$ dotnet build -v q
brace/Program.cs(13,6): error CS1513: } expected [brace/Receipt.csproj]

Build FAILED.

brace/Program.cs(13,6): error CS1513: } expected [brace/Receipt.csproj]
    0 Warning(s)
    1 Error(s)

Time Elapsed …
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 brace_session.py

CS1513, } expected, points at line 13, column 6: just after the brace that the compiler took as the end of the loop. The brace that is really missing belongs after line 11, two lines earlier. Until public static on line 15, which cannot stand inside a method, every line still made sense to the compiler, so it could only notice the gap there and report it after the last brace it had read.

Editing every line the list names

With a missing brace or parenthesis, doing what each later message says adds code that was never wrong. Fix the one cause of the first message, build, and read the new list.

Checkpoint

Check: Fixing a build in rounds

  1. Question 1 of 2 Round 1 of the receipt reported only CS1002, a missing semicolon on line 6. You add the semicolon. What does the next build report?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: CS0029 on line 8 and CS0103 on line 9, which were in the file all along

    Fix the first error, then build again. The recorded second build listed CS0029 and CS0103; the third, after both were fixed, built and printed 3 items, total 245.

  2. Question 2 of 2 In brace/Program.cs, CS1513 } expected points at line 13, column 6. Where is the brace really missing?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: After line 11, where the block of the foreach loop should end

    A position in a diagnostic is where the compiler noticed that something was wrong. With a missing brace that can be lines after the mistake, so look back from the position to where the structure first went wrong.

Warnings compile, and some of them are bugs

A warning does not stop the build, so the program runs, and sometimes it runs into the very problem the warning named. This program greets the name on the first line of its input:

greet/Program.cs C# · greet/Program.cs
// Greets the name on the first line of standard input.
string? name = Console.ReadLine();
Console.WriteLine($"Hello, {name.Trim()}!");
A warning, a crash, and a build that fails on warnings Python · warnings_session.py
from session import run

# greet compiles with one warning. It runs once with a name on standard input and once with no input at all, is built
# a second time without any change, and then greet-strict, the same program with TreatWarningsAsErrors on, is built.
run("""
cd greet
dotnet build -v q
echo Asha | dotnet run
!dotnet run < /dev/null
dotnet build -v q
cd ../greet-strict
!dotnet build -v q
""")

Output

$ cd greet
$ dotnet build -v q
greet/Program.cs(3,29): warning CS8602: Dereference of a possibly null reference. [greet/Greet.csproj]

Build succeeded.

greet/Program.cs(3,29): warning CS8602: Dereference of a possibly null reference. [greet/Greet.csproj]
    1 Warning(s)
    0 Error(s)

Time Elapsed …
$ echo Asha | dotnet run
Hello, Asha!
$ dotnet run < /dev/null
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
   at Program.<Main>$(String[] args) in greet/Program.cs:line 3
[exit status 134]
$ dotnet build -v q

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed …
$ cd ../greet-strict
$ dotnet build -v q
greet-strict/Program.cs(3,29): error CS8602: Dereference of a possibly null reference. [greet-strict/Greet.csproj]

Build FAILED.

greet-strict/Program.cs(3,29): error CS8602: Dereference of a possibly null reference. [greet-strict/Greet.csproj]
    0 Warning(s)
    1 Error(s)

Time Elapsed …
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 warnings_session.py

CS8602 says that name may be null where line 3 calls name.Trim(). With Asha on its input the program greeted her; with no input at all, Console.ReadLine() returned null and the program stopped with a NullReferenceException at the very line the warning named. Then comes a trap: the second dotnet build reported 0 warnings, because nothing had changed, so MSBuild did not run the compiler again, and warnings are only reported by a compiler that runs. A warning you scrolled past once may never be shown again.

greet-strict is the same program with one more line in its project file, <TreatWarningsAsErrors>true</TreatWarningsAsErrors>. Its build turned CS8602 into an error and failed, so no program was written to run. The command-line switch -warnaserror does it for one build, and for the warnings of every build step rather than the compiler’s alone; <WarningsAsErrors>nullable</WarningsAsErrors> makes only the warnings about null fail. The real fix is in the code: string name = Console.ReadLine() ?? ""; gives name a value when the input has ended, and the next lesson reads input properly.

Making the warning go away with !

name!.Trim() silences CS8602, but the crash stays: Microsoft’s reference says that the null-forgiving operator has no effect at run time. Handle the null instead, with ?? or an if (name is null) check.

Checkpoint

Check: Warnings and warnings as errors

  1. Question 1 of 2 greet built with warning CS8602. Without changing anything, you run dotnet build again, and it reports 0 warnings. Why?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Nothing had changed, so MSBuild did not run the compiler, and only a compiler that runs reports warnings

    An up-to-date build skips the compiler, so it reports no warnings. That is one more reason to make warnings fail the build on a build server, where every build compiles from a clean checkout.

  2. Question 2 of 2 Which line in a project file makes every warning fail the build?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: <TreatWarningsAsErrors>true</TreatWarningsAsErrors>

    TreatWarningsAsErrors applies to the compiler's warnings; -warnaserror does the same for one build and for every build step. WarningsAsErrors with nullable limits it to the warnings about null.

A stack trace lists the calls from the throw back to the start

A program that compiles can still fail while it runs. This one averages three marks, and one of them was typed with the letter O instead of a zero:

marks.cs: a FormatException, unhandled C# · marks/marks.cs
// Averages three marks. The second one was typed with the letter O instead of a zero.
string[] lines = ["72", "9O", "85"];
Console.WriteLine($"Average: {Average(lines)}");

static double Average(string[] lines) => Sum(lines) / (double)lines.Length;

static int Sum(string[] lines)
{
    int sum = 0;
    foreach (string line in lines)
    {
        sum += int.Parse(line);
    }
    return sum;
}

Output (exit status 134)

Printed as an error (standard error)

Unhandled exception. System.FormatException: The input string '9O' was not in a correct format.
   at System.Number.ThrowFormatException[TChar](ReadOnlySpan`1 value)
   at System.Int32.Parse(String s)
   at Program.<<Main>$>g__Sum|0_1(String[] lines) in ./marks.cs:line 12
   at Program.<<Main>$>g__Average|0_0(String[] lines) in ./marks.cs:line 5
   at Program.<Main>$(String[] args) in ./marks.cs:line 3

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run marks.cs

Read the trace from the top. The first line names the exception, System.FormatException, and its message, which quotes the text that failed, '9O'. Each line after it that starts with at is a frame: a method that was running when the exception was thrown, newest first. The first two frames, System.Number.ThrowFormatException and System.Int32.Parse, belong to .NET and have no file. The first frame with a file of yours, Sum(String[] lines) in ./marks.cs:line 12, is where to look first; the frames below it are the callers that led there, Average at line 5 and the top-level code at line 3. Names such as <<Main>$>g__Sum|0_1 are how the compiler names a local function of top-level code: read the part after g__. Step through how the program got there and how the exception came back:

Interactive · message by message How marks.cs reaches the line that throws Step through the calls from the top-level code down to Sum, then follow the exception back up. The trace lists the same calls, newest first.
How marks.cs reaches the line that throws, step 6 of 6. Messages so far: Top-level code to Average: Average(lines); Average to Sum: Sum(lines); Sum to Average: FormatException; Average to Top-level code: FormatException.Top-level codeexit status 134Averagea local functionSumthrows at line 12Average(lines)Sum(lines)FormatExceptionFormatException

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Line 3 calls Average. The top-level code reaches line 3 and calls Average(lines). The runtime keeps a frame for each call that has not returned yet: the call stack.
  2. Line 5 calls Sum. Average needs the sum first, so line 5 calls Sum(lines). Now three frames are waiting: the top-level code, Average and Sum.
  3. int.Parse throws. Sum adds 72, then calls int.Parse on "9O" at line 12. The O is a letter, so int.Parse throws a FormatException, and Sum stops in the middle of its loop.
  4. Sum has no catch. No try and catch surrounds line 12, so Sum ends at once and the exception goes on to its caller, Average, without a return value.
  5. Average has none either. Average has no catch for it, so the exception moves up again, to the top-level code at line 3.
  6. Nothing caught it. No frame caught the exception. The runtime prints Unhandled exception, the type and message, then one at line per frame, newest first: Sum line 12, Average line 5, the top-level code line 3. The process ends with exit status 134.

No catch stopped the exception, so the runtime printed the trace to standard error and ended the process with exit status 134. The Bash manual gives the rule behind that number: a program ended by signal N exits with 128 + N, and signal 6 is SIGABRT, the abort signal, which the runtime raised to end this run on macOS.

The file and line numbers come from the .pdb file that the build writes next to the .dll: Microsoft’s compiler reference gives portable as the default DebugType of both Debug and Release builds. A Release build is optimized, and the documentation of Exception.StackTrace warns that optimizations such as inlining can leave out some of the method calls you expect.

Rethrowing with throw ex

In a catch block, throw ex; restarts the stack trace at that line, and the frames down to the original throw are lost. Write throw; to rethrow the exception as it is. The SDK’s analyzer warns about the first form as CA2200.

Checkpoint

Check: Reading a stack trace

  1. Question 1 of 2 The trace of marks.cs lists System.Number.ThrowFormatException, System.Int32.Parse, then Sum at ./marks.cs:line 12, Average at line 5 and the top-level code at line 3. Where do you look first?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Line 12 of marks.cs, in Sum: the first frame in a file of yours

    Read a trace from the top: the exception and its message, then the frames, newest first. Skip the frames of .NET itself to the first frame of your own code, and use the frames below it to see how the program got there.

  2. Question 2 of 2 In what order does a .NET stack trace list its frames?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Newest first: the method that threw, then its caller, then that caller's caller

    Each line starting with at is a frame of the call stack when the exception was thrown, from the newest call to the oldest.

Some failures end the process before any catch can run

Depth below calls itself with no case that stops it, inside a try block whose catch should print what went wrong:

depth.cs: a stack overflow C# · depth/depth.cs
// Depth calls itself with no case that stops it. The catch block below never runs.
try
{
    Console.WriteLine(Depth(1));
}
catch (Exception e)
{
    Console.WriteLine($"Caught {e.GetType().Name}");
}

static int Depth(int n) => Depth(n + 1) + 1;

Output (exit status 134)

Printed as an error (standard error)

Stack overflow.
Repeated 261387 times:
--------------------------------
   at Program.<<Main>$>g__Depth|0_0(Int32)
--------------------------------
   at Program.<Main>$(System.String[])

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run depth.cs

Caught never appeared. The runtime printed Stack overflow., the one frame it found repeated 261,387 times, and the frame that made the first call, then ended the process with exit status 134. The number depends on the size of the thread’s stack and of each frame. Microsoft’s documentation of StackOverflowException is plain about it: you can’t catch one with try and catch, and the process is terminated, so code that recurses must stop itself, with a case that ends the recursion or a loop instead.

Debug.Assert ends a process too, but only in some builds. This program checks each mark with it, and average-checked checks it with an exception instead:

average/Program.cs C# · average/Program.cs
using System.Diagnostics;

int[] marks = [72, 105, 85]; // 105 is not a valid mark
int sum = 0;
foreach (int mark in marks)
{
    Debug.Assert(mark is >= 0 and <= 100, $"mark {mark} is out of range");
    sum += mark;
}
Console.WriteLine($"Average: {sum / (double)marks.Length:F1}");
average-checked/Program.cs C# · average-checked/Program.cs
int[] marks = [72, 105, 85]; // 105 is not a valid mark
int sum = 0;
foreach (int mark in marks)
{
    if (mark is < 0 or > 100)
    {
        throw new ArgumentOutOfRangeException(nameof(marks), mark, "A mark is from 0 to 100.");
    }
    sum += mark;
}
Console.WriteLine($"Average: {sum / (double)marks.Length:F1}");
Debug.Assert and an exception, in Debug and Release builds Python · assert_session.py
from session import run

# average checks each mark with Debug.Assert; average-checked throws an exception instead. Each runs as a Debug build,
# the default of dotnet run, and as a Release build.
run("""
cd average
!dotnet run
dotnet run -c Release
cd ../average-checked
!dotnet run
!dotnet run -c Release
""")

Output

$ cd average
$ dotnet run
Process terminated.
Assertion failed.
mark 105 is out of range
   at Program.<Main>$(String[] args) in average/Program.cs:line 7
[exit status 134]
$ dotnet run -c Release
Average: 87.3
$ cd ../average-checked
$ dotnet run
Unhandled exception. System.ArgumentOutOfRangeException: A mark is from 0 to 100. (Parameter 'marks')
Actual value was 105.
   at Program.<Main>$(String[] args) in average-checked/Program.cs:line 7
[exit status 134]
$ dotnet run -c Release
Unhandled exception. System.ArgumentOutOfRangeException: A mark is from 0 to 100. (Parameter 'marks')
Actual value was 105.
   at Program.<Main>$(String[] args) in average-checked/Program.cs:line 7
[exit status 134]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 assert_session.py

The Debug build, the default of dotnet run, stopped at the assertion: Process terminated., Assertion failed., the message and line 7, with exit status 134. The Release build printed Average: 87.3 and exit status 0, the average of 72, 105 and 85, with the invalid mark included. The methods of Debug carry the Conditional("DEBUG") attribute, so the compiler leaves their calls out unless the DEBUG symbol is defined, and by default it is defined for Debug builds only. The exception of average-checked stopped both builds at the same line.

Check In a Debug build In a Release build Use it for
Debug.Assert(condition, message) ends the process when false removed: 105 was averaged assumptions about your own code while you develop it
throw new ArgumentOutOfRangeException(…) throws throws the same input from users, files and other code

Key idea

A stack overflow cannot be caught, and Debug.Assert does not exist in a Release build: stop recursion yourself, and check anything that can go wrong in a shipped program with code that throws or returns an error.

Checkpoint

Check: Stack overflows and Debug.Assert

  1. Question 1 of 2 depth.cs calls Depth(1) inside try with catch (Exception e). Depth calls itself with no case that stops it. What happens?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The process prints Stack overflow. and ends with exit status 134; the catch block never runs

    Microsoft's documentation of StackOverflowException says it cannot be caught and the process is terminated, so recursive code must stop itself with a case that ends it, or become a loop.

  2. Question 2 of 2 A Release build runs Debug.Assert(mark is >= 0 and <= 100, ...) on a mark of 105. What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Nothing: the call is not in the Release build, so 105 is used

    Debug.Assert checks your own assumptions in Debug builds. Input that can be wrong in a shipped program needs a check that stays in every build, such as an exception.

A debugger pauses the program so you can look inside

A wrong value with no error is the hardest kind of bug to see in the output. This program prints the average of two marks:

halves.cs: an average that comes out wrong C# · halves/halves.cs
// Prints the average of two marks: it prints 78, but the average of 72 and 85 is 78.5.
int[] marks = [72, 85];
int sum = 0;
foreach (int mark in marks)
{
    sum += mark;
}
double average = sum / marks.Length;
Console.WriteLine($"Average: {average}");

Output

Average: 78

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run halves.cs

It printed 78, and the average of 72 and 85 is 78.5. You could add Console.WriteLine calls and run it again until the wrong step shows, or ask a debugger: A tool that runs a program under its control, so that it can pause the program at chosen lines and show the values of variables, the call stack and any expression you ask about. to stop the program at a line and show you every variable there. Visual Studio, Rider, and VS Code with Microsoft’s C# extension all include one. Step through a session in VS Code:

Interactive · message by message Finding why halves.cs prints 78 Step through one debugging session. Watch what the debugger shows each time the program is paused.
Finding why halves.cs prints 78, step 6 of 6. Messages so far: You to Debugger: F9 on line 8; Debugger to halves.cs: start; halves.cs to Debugger: paused at line 8; You to Debugger: F10.Youin the editorDebuggerVS Code and C#halves.csaverage = 78F9 on line 8startpaused at line 8F10(double) gives 78.5

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Set a breakpoint. Click the margin left of line 8, or put the cursor on it and press F9. A red dot marks a breakpoint: the program will pause before that line runs.
  2. Start debugging. Press F5. The debugger builds the program in its Debug configuration and starts it under its control, so that it can stop it.
  3. Paused before line 8. The loop has run twice and line 8 is next. The Variables view shows sum = 157, and the Call Stack view shows one frame, the top-level code.
  4. Step over. Press F10 to run line 8 and pause again before line 9. The Variables view now shows average = 78, not the 78.5 you expected.
  5. Ask the Watch view. Add sum / marks.Length to the Watch view: it shows 78. Both sides of / are int, so C# divides whole numbers and drops the .5 before the result is stored in the double.
  6. Try the fix in place. Add a second watch, sum / (double)marks.Length: it shows 78.5. Stop the session with Shift+F5, put the cast into line 8 and run the program again.

A breakpoint: A mark on a line of code that makes a debugger pause the program just before that line runs. A conditional breakpoint pauses only when an expression you give it is true. pauses the program before its line runs. From there, F10 steps over the current line, F11 steps into a method that the line calls, Shift+F11 steps out to the caller and F5 runs on to the next breakpoint. These are the default keys of VS Code and Visual Studio; other editors have the same commands, sometimes on other keys. The Variables view shows the values of the paused frame, the Watch view evaluates any expression you type each time the program pauses, and the Call Stack view is the stack trace of a program that has not failed yet. A conditional breakpoint pauses only when an expression you give it is true, such as mark > 100 in a loop over many marks. For exceptions, the Breakpoints view of the C# debugger in VS Code offers All Exceptions: ticked, it pauses where an exception is thrown, before any catch handles it, so a catch cannot hide the line from you.

Debug halves.cs yourself

Make a console project with dotnet new console -o halves, replace its Program.cs with halves.cs, open the folder in VS Code with the C# extension, set a breakpoint on line 8 and press F5. Add sum / marks.Length and sum / (double)marks.Length to the Watch view, then change line 8 and run it again.

Checkpoint

Check: Breakpoints, stepping and watches

  1. Question 1 of 2 The program is paused before a line that calls Average(marks). Which key, in VS Code or Visual Studio, runs that whole line and pauses before the next one?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: F10, Step Over

    F10 steps over, F11 steps into, Shift+F11 steps out and F5 continues to the next breakpoint, by default in VS Code and Visual Studio.

  2. Question 2 of 2 A loop reads 10,000 marks and one of them is over 100. How do you pause only at that one?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A conditional breakpoint inside the loop, with the expression mark > 100

    A breakpoint can carry a condition or a hit count, so the debugger stops at the one pass you need instead of every pass of the loop.

Practise: the kata, and questions on all six ideas

This program has four problems: two that stop the build, one that stops the program while it runs, and one wrong value. Its comments say what it should print:

kata/Program.cs C# · kata/Program.cs
// Kata: four problems stand between this program and the two lines it should print:
//   Rows: 3
//   Average: 82.3
string[] rows = ["Asha,72", "Ravi,85", "Meera,9O"];
int total = 0;
foreach (string row in rows)
{
    string[] parts = row.Split(',');
    int mark = int.Parse(parts[1]);
    total += mark
}
double average = total / rows.Length;
Console.WriteLine($"Rows: {row.Length}");
Console.WriteLine($"Average: {average:F1}");
The kata as you get it Python · kata_session.py
from session import run

# The kata as you get it: its first build.
run("""
cd kata
!dotnet build -v q
""")

Output

$ cd kata
$ dotnet build -v q
kata/Program.cs(10,18): error CS1002: ; expected [kata/Kata.csproj]

Build FAILED.

kata/Program.cs(10,18): error CS1002: ; expected [kata/Kata.csproj]
    0 Warning(s)
    1 Error(s)

Time Elapsed …
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 kata_session.py

Copy the folder, then fix it round by round: the first build names line 10. Build again after each fix, read the stack trace when the program fails at run time, and use a watch on total / rows.Length when the average still comes out wrong. First hint: the last problem is the one halves.cs had.

After the kata, the questions below cover all six ideas, starting with the one your earlier answers show you know least; every choice says why it is right or what it misses:

Practice that adapts to you

Practise this lesson

14 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 14 What does the word error in round-1/Program.cs(6,19): error CS1002: ; expected tell you?

    Choose one answer.

    Show the answer: Question 1 of 14

    Answer: The build failed and no program was written

    The severity is error or warning. Errors stop the build; warnings let it finish and the program run.

  2. Question 2 of 14 greet built with greet/Program.cs(3,29): warning CS8602. Line 3 is Console.WriteLine($"Hello, {name.Trim()}!"); Which part of the line does column 29 point at?

    Choose one answer.

    Show the answer: Question 2 of 14

    Answer: The start of name, inside the braces

    The column counts characters from 1 at the start of the line. Here it lands on name, the reference that may be null when name.Trim() runs.

  3. Question 3 of 14 After a team turns on warnings as errors, a build fails with error CA2200 in code that compiled for years. What does the id tell you?

    Choose one answer.

    Show the answer: Question 3 of 14

    Answer: A code analyzer rule of the SDK reported it, as a warning made into an error

    The prefix sends you to the right documentation: CA2200 is the analyzer rule Rethrow to preserve stack details, which the SDK enables as a warning, and warnings as errors made the build fail on it.

  4. Question 4 of 14 A build lists 14 errors. Which do you work on first?

    Choose one answer.

    Show the answer: Question 4 of 14

    Answer: The first one in the list

    Fix the first diagnostic, build again and read the new list, round by round.

  5. Question 5 of 14 You fix the only error of a build, a missing parenthesis, and the next build reports three new errors in other lines. What is most likely?

    Choose one answer.

    Show the answer: Question 5 of 14

    Answer: The fix was right; the syntax error had hidden mistakes of meaning that the compiler can now see

    Errors come in layers. A file must parse before the compiler can check what its names and types mean, so new errors after a syntax fix are progress.

  6. Question 6 of 14 A build ends with 1 Warning(s) and 0 Error(s). Is there a program to run?

    Choose one answer.

    Show the answer: Question 6 of 14

    Answer: Yes: warnings do not stop the build, so the program is written and runs

    A warning is a problem the compiler suspects but does not refuse. Read it: CS8602 named the very line that crashed.

  7. Question 7 of 14 You change name.Trim() in greet to name!.Trim() and run the program with no input. What happens?

    Choose one answer.

    Show the answer: Question 7 of 14

    Answer: The same NullReferenceException at line 3; only the warning has gone

    ! tells the compiler not to warn; it does not change what happens. Handle the null with ?? or an if instead.

  8. Question 8 of 14 With which exit status did marks.cs end on macOS after its unhandled FormatException?

    Type a number.

    Show the answer: Question 8 of 14

    Answer: 134

    The runtime ended the process with signal 6, SIGABRT, and Bash reports a program ended by signal N with exit status 128 + N, so 134.

  9. Question 9 of 14 A Release build deployed to a server logs a stack trace whose frames have no file names or line numbers. What is most likely missing?

    Choose one answer.

    Show the answer: Question 9 of 14

    Answer: The .pdb files that the build writes next to the .dll files were not deployed

    Ship the .pdb files with the program, or embed them with DebugType embedded, and the frames keep their files and lines. Optimizations such as inlining may still leave out a frame.

  10. Question 10 of 14 A catch block logs an exception and then runs throw ex;. The trace that reaches the top shows the catch block's method but not the method that threw. Why?

    Choose one answer.

    Show the answer: Question 10 of 14

    Answer: throw ex; restarts the stack trace at that line; throw; would keep the original frames

    Rethrow with throw; inside the catch block. CA2200 flags throw ex; because the frames between the original throw and the rethrow are lost.

  11. Question 11 of 14 What is Debug.Assert for?

    Choose one answer.

    Show the answer: Question 11 of 14

    Answer: Checking assumptions about your own code while you develop and test it

    Use Debug.Assert for what can only be false if your own code has a bug, and real checks for input.

  12. Question 12 of 14 A recursive function walks a tree of folders and crashes with Stack overflow. on a customer's very deep tree. What fix is robust?

    Choose one answer.

    Show the answer: Question 12 of 14

    Answer: Walk the tree with a loop and a Stack<T> of folders still to visit

    When the depth comes from data you do not control, replace recursion with a loop and an explicit stack, or check the depth and stop with an error you choose.

  13. Question 13 of 14 You set a breakpoint on line 8 of halves.cs and press F5. When does the program pause?

    Choose one answer.

    Show the answer: Question 13 of 14

    Answer: Just before line 8 runs

    A breakpoint pauses execution before its line runs, so you see the state that line will work with.

  14. Question 14 of 14 Paused before line 8 of halves.cs, where sum is 157, which watch expression shows the average the program should store?

    Choose one answer.

    Show the answer: Question 14 of 14

    Answer: sum / (double)marks.Length, which shows 78.5

    A watch evaluates any expression in the paused frame, so you can try a fix before you edit the code.

Interview questions

Fresher · What is the difference between a compiler error and a warning in C#, and what does TreatWarningsAsErrors change?

An error means the compiler cannot build the code, such as a missing semicolon (CS1002) or a name that does not exist (CS0103): the build fails and no program is written. A warning means the code compiles but probably has a problem, such as CS8602 when a reference that may be null is used; the build succeeds and the program can still fail at that line when it runs. Each diagnostic names its file, line and column, its severity and an id. Setting <TreatWarningsAsErrors>true</TreatWarningsAsErrors> in the project file, or building with -warnaserror, reports every warning as an error, so a build with warnings fails. Teams do it on build servers because nobody reads the warnings of a green build, and an up-to-date build does not even show them again.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A service crashes in production. Its log ends without any error line, and its console shows "Stack overflow." with one frame repeated thousands of times. Why did the logging in your catch blocks never run, and how do you find and fix the cause?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A teammate checked every record of an import with Debug.Assert. Runs from the IDE stopped on bad records, but the production service, a Release build, imported 1,200 invalid rows without a word. Explain what happened and how you would change the checks.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A diagnostic names the file, the line and column, the severity, an id whose letters say which tool reported it, and a message that says what was expected.
  • Fix the first error and build again: later errors may be consequences, and errors of meaning can wait behind a syntax error, as CS0029 and CS0103 waited behind CS1002.
  • A warning can be a crash that compiles; an up-to-date build does not show it again, and TreatWarningsAsErrors makes it fail the build.
  • A stack trace lists the frames newest first; the first frame with a file of yours is where to look, and an unhandled exception ended these programs with exit status 134.
  • A stack overflow cannot be caught, and Debug.Assert disappears from Release builds, so input is checked with code that throws or returns an error.
  • A debugger pauses before a breakpoint’s line and shows variables, watches and the call stack, which found the integer division in halves.cs.

Terms in this lesson

breakpoint
A mark on a line of code that makes a debugger pause the program just before that line runs. A conditional breakpoint pauses only when an expression you give it is true.
debugger
A tool that runs a program under its control, so that it can pause the program at chosen lines and show the values of variables, the call stack and any expression you ask about.
diagnostic
A message from a build tool about your code: its file, line and column, a severity (error or warning), an id such as CS1002 and a text. An error stops the build; a warning does not.
stack trace
The list of method calls that were running when an exception was thrown, newest first, each with its file and line when debug symbols are available. An unhandled exception prints it to standard error.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.