Debugging Fundamentals in C#

Vaibhav • September 10, 2025

In the previous article, we explored best practices for exception handling - how to throw, catch, and log errors responsibly. But even with great exception handling, bugs still happen. That’s where debugging comes in. Debugging is the process of finding and fixing problems in your code. It’s a skill every developer must master, and in this article, we’ll walk through the fundamentals of debugging in C# using Visual Studio and the .NET ecosystem.

We’ll cover how to set breakpoints, inspect variables, step through code, and understand the call stack. These tools help you see what your program is doing - and why it’s not doing what you expect.

What Is Debugging?

Debugging is the act of running your program in a controlled environment so you can observe its behavior and identify problems. It’s not just about fixing syntax errors - it’s about understanding logic errors, unexpected values, and runtime failures.

In C#, debugging is typically done using an Integrated Development Environment (IDE) like Visual Studio. The debugger lets you pause execution, inspect variables, and step through code line by line.

Debugging is not just for fixing bugs - it’s also a powerful way to learn how your code works. Beginners often discover new insights by stepping through their programs.

Setting Breakpoints

A breakpoint is a marker that tells the debugger to pause execution at a specific line of code. You can set a breakpoint by clicking in the left margin of the code editor in Visual Studio, or by pressing F9 on the line you want to pause.

static void Main()
{
    int x = 5;
    int y = 0;
    int result = x / y; // Set breakpoint here
    Console.WriteLine("Result: " + result);
}

When you run the program in debug mode (F5), execution will pause at the breakpoint before the division occurs. This lets you inspect the values of x and y before the error happens.

Stepping Through Code

Once your program is paused, you can step through it line by line using the following commands:

  • Step Over (F10): Executes the current line and moves to the next, skipping over method calls.
  • Step Into (F11): Enters the method being called so you can debug inside it.
  • Step Out (Shift+F11): Finishes the current method and returns to the caller.

These commands let you control the flow of execution and observe how your program behaves at each step.

Use Step Into when you want to debug a method’s internal logic. Use Step Over when you trust the method and just want to move past it.

Inspecting Variables

While paused at a breakpoint, you can hover over variables to see their current values. You can also use the Locals and Autos windows to view all variables in scope.

For example, if you’re debugging this code:

int a = 10;
int b = 0;
int c = a / b;

You can hover over a, b, and c to see their values. If b is zero, you’ll know why the division fails.

You can also add variables to the Watch window to monitor them as you step through the code. This is useful for tracking changes over time.

Using the Call Stack

The Call Stack window shows the sequence of method calls that led to the current point in execution. It’s like a breadcrumb trail that helps you understand how you got here.

static void Main()
{
    Level1();
}

static void Level1()
{
    Level2();
}

static void Level2()
{
    throw new Exception("Something went wrong.");
}

When the exception is thrown, the call stack will show:

  • Level2
  • Level1
  • Main

This helps you trace the path your program took and identify where the error originated.

Using Conditional Breakpoints

Sometimes you only want to pause execution when a certain condition is true. That’s where conditional breakpoints come in. Right-click a breakpoint and choose “Conditions” to set a rule.

for (int i = 0; i < 100; i++)
{
    Console.WriteLine(i);
}

You can set a breakpoint that only triggers when i == 42. This saves time and lets you focus on the case you care about.

You can also set breakpoints that trigger only after being hit a certain number of times - useful for debugging loops.

Using Exception Settings

Visual Studio lets you control how exceptions are handled during debugging. Open the Exception Settings window to choose which exceptions should break into the debugger.

For example, you can tell Visual Studio to break on all System.NullReferenceException errors, even if they’re caught. This helps you find the root cause faster.

You can also enable “Just My Code” to ignore system libraries and focus on your own code.

Using Debug.WriteLine

Sometimes you want to log information without using breakpoints. The Debug.WriteLine method writes messages to the Output window during debugging.

Debug.WriteLine("Starting calculation...");
int result = Compute();
Debug.WriteLine("Result: " + result);

This is useful for tracing execution flow or printing variable values without pausing the program.

Common Debugging Mistakes

Debugging is a skill, and like any skill, it’s easy to make mistakes. Here are some common pitfalls:

  • Setting breakpoints in the wrong place - make sure they’re on executable lines.
  • Forgetting to run in debug mode - F5 starts debugging, Ctrl+F5 does not.
  • Ignoring the call stack - it’s your best friend when tracing errors.
  • Not inspecting variables - hover, watch, and use the Locals window.
  • Swallowing exceptions - don’t catch and ignore without logging.

When debugging, slow down and observe. Don’t just fix the symptom - understand the cause.

Debugging Asynchronous Code

Debugging async methods can be tricky because the call stack may not show the full path. Visual Studio helps by showing async call chains and letting you step through await statements.

async Task LoadDataAsync()
{
    string content = await File.ReadAllTextAsync("data.txt");
    Console.WriteLine(content);
}

You can set breakpoints before and after the await to see how control flows. The debugger will pause when the task completes and resumes execution.

Debugging Exceptions

When an exception occurs, Visual Studio shows a dialog with the error message and stack trace. You can click “Break” to pause execution and inspect the state.

Use the Exception Settings window to control which exceptions break into the debugger. This helps you catch errors early and understand their context.

Summary

Debugging is an essential skill for every C# developer. It’s how you find and fix problems, understand your code, and build confidence in your logic. In this article, you’ve learned how to set breakpoints, step through code, inspect variables, use the call stack, and debug exceptions. You’ve also seen how to use conditional breakpoints, Debug.WriteLine, and exception settings to make debugging more effective.

With these tools and techniques, you’ll be able to diagnose issues faster, write better code, and become a more confident developer. In the next article, we’ll explore Visual Studio Debugging - diving deeper into the IDE’s advanced features like live expressions, data tips, and performance profiling.