Writing code is only part of programming. A large amount of development time is spent understanding why code does not behave as expected.
That process is called debugging.
Good debugging is not about randomly changing code until an error disappears. It is a systematic process of reproducing a problem, collecting evidence, finding its cause, fixing it and verifying that the fix has not introduced another problem.
In this guide, we'll walk through practical debugging techniques using simple examples, including C# examples that you can try yourself.
What Is Debugging?
Debugging is the process of finding, understanding and correcting defects in software.
A typical debugging process looks like this:
Problem Reported
↓
Reproduce Problem
↓
Collect Evidence
↓
Locate Problem Area
↓
Identify Root Cause
↓
Fix Code
↓
Test the Fix
↓
Verify No Regression
The important part is finding the root cause, rather than only hiding the visible symptom.
Types of Programming Errors
Before debugging a problem, it helps to understand what kind of error you're dealing with.
| Error Type | Example | When Detected |
|---|---|---|
| Syntax / Compile Error | Invalid C# syntax | During compilation |
| Runtime Error | NullReferenceException | While application runs |
| Logic Error | Wrong calculation | Usually during testing |
| Performance Problem | Slow database query | Testing or production |
| Configuration Error | Incorrect connection string | Startup or runtime |
1. Reproduce the Bug First
One of the most important debugging rules is:
Understand how to reproduce the problem before trying to fix it.
For example, imagine a user reports:
"The application crashes when I save a customer."
That isn't enough information. Try to determine:
- Which customer data causes it?
- Does it happen every time?
- Does it happen only for certain users?
- Which browser or environment is affected?
- What happened immediately before the error?
A reproducible bug is usually much easier to investigate.
2. Read the Error Message Carefully
Developers sometimes skip the error message and immediately start changing code. That can waste time.
Consider this C# code:
string? name = null;
Console.WriteLine(name.Length);
You may get an exception such as:
System.NullReferenceException
The exception already provides an important clue: the code attempted to access a member through a null reference.
Don't read only the exception name. Also inspect:
- Exception message
- Stack trace
- File name
- Line number
- Inner exception, when present
3. Understand the Stack Trace
A stack trace shows the sequence of method calls that led to an exception.
A simplified example might look like:
System.NullReferenceException
at CustomerService.GetCustomerName()
at OrderService.CreateOrder()
at OrdersController.Create()
Instead of searching the entire application, the stack trace gives you a starting point.
Look first for frames belonging to your own application. Framework calls may appear in the trace too, but the relevant application code is often where the investigation should begin.
4. Use Breakpoints
A breakpoint pauses program execution at a selected line so that you can inspect what is happening at that moment.
Suppose you have:
public decimal CalculateTotal(decimal price, int quantity)
{
decimal total = price * quantity;
return total;
}
Place a breakpoint on:
decimal total = price * quantity;
When execution stops, inspect:
price
quantity
total
This helps answer an important debugging question: Are the values what I expected them to be?
5. Step Through the Code
Once execution is paused, debuggers usually provide commands such as:
| Command | Purpose |
|---|---|
| Continue | Resume execution |
| Step Over | Execute the current line without entering called methods |
| Step Into | Enter the method being called |
| Step Out | Finish the current method and return to its caller |
Stepping through code lets you compare what you think the program does with what it actually does.
6. Inspect Variables
Many bugs become obvious when you inspect variable values.
Imagine this code:
int quantity = 5;
decimal price = 100;
decimal discount = 20;
decimal total = quantity * price - discount;
While debugging, inspect each variable and confirm that its value matches your assumptions.
Modern IDEs also provide Watch, Locals and similar windows that allow you to monitor values while stepping through code.
7. Use Conditional Breakpoints
A normal breakpoint may be inconvenient when a loop executes thousands of times.
For example:
for (int i = 0; i < 10000; i++)
{
ProcessItem(i);
}
Suppose the problem occurs only when:
i == 7825
Instead of manually continuing thousands of times, configure a conditional breakpoint that pauses only when the condition is true.
8. Use Logging
Breakpoints are excellent during development, but many bugs happen in environments where attaching a debugger is impractical.
This is where logging becomes essential.
For example, in ASP.NET Core:
public class OrderService
{
private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger)
{
_logger = logger;
}
public void ProcessOrder(int orderId)
{
_logger.LogInformation(
"Processing order {OrderId}",
orderId);
}
}
Useful logs can help answer:
- Which operation failed?
- When did it fail?
- Which request was being processed?
- Which branch of the application executed?
- What exception occurred?
9. Don't Log Sensitive Information
Logging everything is not good debugging.
Avoid unnecessarily recording sensitive information such as:
- Passwords
- Authentication tokens
- API secrets
- Private keys
- Full payment-card details
- Other sensitive personal data
Logs should provide enough information to investigate a problem without creating a new security or privacy problem.
10. Use Print or Console Statements
Sometimes a simple output statement is enough for a small program.
Console.WriteLine($"User ID: {userId}");
Console.WriteLine($"Order total: {total}");
This can be useful for learning and quick experiments.
For production applications, structured application logging is generally more useful because it provides levels, timestamps, searchable properties and integration with monitoring systems.
11. Isolate the Problem
Large applications contain many components. Instead of investigating everything simultaneously, reduce the problem.
Suppose your application flow is:
Browser
↓
Controller
↓
Service
↓
Repository
↓
Database
Ask where the first incorrect result appears.
If the repository returns the correct data but the service returns the wrong result, you've dramatically reduced the area you need to inspect.
12. Use a Divide-and-Conquer Approach
For a large block of logic, you can progressively narrow down where the problem starts.
Entire Process
↓
Which Half Fails?
↓
Which Section?
↓
Which Method?
↓
Which Statement?
This is sometimes informally described as using a binary-search approach to debugging.
The objective isn't to randomly delete code. It is to systematically reduce the search area.
13. Check Your Assumptions
Many difficult bugs survive because the developer assumes something must be true.
Examples:
"This value can never be null."
"This API always returns HTTP 200."
"This list always contains at least one item."
"This database record always exists."
"This method is called only once."
During debugging, verify these assumptions instead of treating them as facts.
14. Debug NullReferenceException in C#
Consider:
Customer? customer = GetCustomer();
Console.WriteLine(customer.Name);
If GetCustomer() returns null, accessing
customer.Name can throw an exception.
A safer version depends on your application's intended behavior:
Customer? customer = GetCustomer();
if (customer is null)
{
Console.WriteLine("Customer was not found.");
return;
}
Console.WriteLine(customer.Name);
But don't blindly add null checks everywhere. Ask why the value is null and whether null is actually a valid state.
15. Debug Incorrect Calculations
Not every bug throws an exception.
Consider:
int total = 100;
int discountPercent = 20;
int finalPrice = total - discountPercent;
The application runs, but the calculation is wrong if
discountPercent represents a percentage.
One possible correction is:
decimal total = 100m;
decimal discountPercent = 20m;
decimal discount =
total * discountPercent / 100m;
decimal finalPrice =
total - discount;
Console.WriteLine(finalPrice);
This demonstrates why logic errors can be harder to find than exceptions: the program runs successfully but produces the wrong result.
16. Debug Loops Carefully
A classic problem is an incorrect loop boundary.
string[] names =
{
"Alex",
"Sam",
"Taylor"
};
for (int i = 0; i <= names.Length; i++)
{
Console.WriteLine(names[i]);
}
The last iteration attempts to access an index that doesn't exist.
It should be:
for (int i = 0; i < names.Length; i++)
{
Console.WriteLine(names[i]);
}
When debugging loops, pay special attention to initial values, conditions, increments and collection boundaries.
17. Debug API Problems Layer by Layer
When an API request fails, separate the problem into layers.
Client Request
↓
Routing
↓
Authentication
↓
Controller
↓
Business Logic
↓
Database / External API
↓
Response
Check questions such as:
- Did the request reach the server?
- Was the correct endpoint selected?
- Was authentication successful?
- Did model validation fail?
- Did the service throw an exception?
- Did the database query succeed?
- What HTTP status code was returned?
This is much more effective than treating "the API doesn't work" as one large problem.
18. Understand HTTP Status Codes
When debugging web APIs, the HTTP response often gives you an immediate direction.
| Status | Typical Meaning |
|---|---|
| 400 | Invalid request |
| 401 | Authentication required or failed |
| 403 | Authenticated but not permitted |
| 404 | Resource or route not found |
| 500 | Server encountered an error |
The status code doesn't always identify the root cause, but it helps narrow your investigation.
19. Use Exception Handling Correctly
Avoid swallowing exceptions like this:
try
{
ProcessOrder();
}
catch
{
}
Now the application may fail silently and valuable debugging information is lost.
Handle an exception when you can meaningfully recover, translate it into appropriate application behavior, or add useful context. Otherwise, allow your application's centralized error handling and logging strategy to deal with it appropriately.
20. Compare With the Last Working Version
If something worked yesterday and fails today, ask:
What changed?
Check changes to:
- Application code
- Configuration
- Dependencies
- Database schema
- Environment variables
- Infrastructure
- Deployment settings
Version control systems such as Git make this investigation much easier.
21. Write a Test for the Bug
When possible, create a test that reproduces the incorrect behavior.
The workflow becomes:
Bug Found
↓
Create Failing Test
↓
Fix Bug
↓
Test Passes
↓
Keep Test to Detect Regression
This provides evidence that your change fixes the problem and helps prevent the same bug from returning unnoticed.
22. Use Rubber Duck Debugging
Rubber duck debugging means explaining the problem step by step as though you were explaining it to another person—or even to an object on your desk.
Forcing yourself to explain:
- what should happen,
- what actually happens,
- what each line does, and
- why you believe each assumption is true
can expose gaps in your reasoning.
23. Read the Documentation
Sometimes there is no mysterious software bug. The API or library simply doesn't behave the way you assumed.
Check the official documentation for:
- Method behavior
- Parameter requirements
- Return values
- Exceptions
- Configuration
- Version-specific behavior
A Practical Debugging Checklist
When you encounter a bug, work through this sequence:
- Reproduce the problem.
- Record the expected behavior.
- Record the actual behavior.
- Read the complete error message.
- Inspect the stack trace.
- Check relevant logs.
- Find the first point where data becomes incorrect.
- Use breakpoints and inspect variables.
- Reduce the problem to the smallest reproducible case.
- Verify your assumptions.
- Identify the root cause.
- Make the smallest appropriate fix.
- Test the original scenario again.
- Test related scenarios for regressions.
Common Debugging Mistakes
Changing Multiple Things at Once
If you modify five things and the problem disappears, you may not know which change actually fixed it.
Make controlled changes whenever practical.
Fixing the Symptom Instead of the Cause
If an object unexpectedly becomes null, adding a null check might prevent the exception—but it may not explain why the object is missing.
Investigate the cause.
Ignoring the Stack Trace
The stack trace is one of your most useful sources of debugging evidence. Don't discard it.
Assuming Production Is Identical to Development
A problem may depend on:
- Configuration
- Data
- Permissions
- Network behavior
- Dependency versions
- Operating system
- Deployment environment
Always consider environmental differences.
Debugging vs Testing
Testing and debugging are related, but they are not the same thing.
| Testing | Debugging |
|---|---|
| Detects unexpected behavior | Investigates why it happens |
| Verifies requirements | Finds root causes |
| Can be automated | Often requires investigation |
Good tests can reveal bugs. Debugging helps you understand and fix them.
Frequently Asked Questions
What is the first step in debugging?
Start by reproducing the problem consistently and defining the expected and actual behavior. Without that information, debugging often becomes guesswork.
What is a breakpoint?
A breakpoint tells a debugger to pause program execution at a particular location so you can inspect variables, call stacks and application state.
What is a stack trace?
A stack trace shows the chain of method calls active when an exception occurred. It can help identify where an error originated and how execution reached that point.
Is Console.WriteLine a good debugging method?
It can be useful for small programs and quick experiments. For larger or production applications, structured logging and debugger tools are usually more effective.
Why does code work locally but fail in production?
The environments may differ in configuration, permissions, data, dependencies, network access, operating system, infrastructure or other runtime conditions. Compare those differences systematically.
How can I become better at debugging?
Practice investigating problems systematically. Learn your debugger, understand stack traces, improve logging, read documentation and focus on evidence rather than assumptions.
Conclusion
Debugging is one of the most valuable skills a programmer can develop. The goal isn't simply to make an error disappear—it is to understand why the software behaved incorrectly.
Start by reproducing the problem. Read the error and stack trace, inspect variables, use breakpoints and logs, isolate the failing component and verify every important assumption.
The more systematic your debugging process becomes, the less time you'll spend randomly changing code and the faster you'll reach the actual root cause.