DEV Community

Cover image for Why stubborn bugs waste days
Sukhpinder Singh for C# Programming

Posted on

Why stubborn bugs waste days

Chasing symptoms when debugging can be an inefficient path. Instead of relying on intuition and making chaotic changes, a systematic routine can reduce the problem space and help pinpoint the root cause. These guidelines are useful for addressing performance issues, data races, network timeouts, and UI freezes. While the examples use .NET 8 and Windows, the core ideas are applicable across different platforms.

1: Freeze the Environment and Record the Scene

To begin, establish a controlled environment. Prevent any further changes by stopping deployments and isolating the issue to a specific machine or container to act as a lab. It’s important to record the exact versions of software and commands being used.

# Environment snapshot
dotnet --info
git rev-parse --short HEAD
set | findstr /R ASPNETCORE_ENVIRONMENT|DOTNET_
Enter fullscreen mode Exit fullscreen mode

If the error involves time or random numbers, set a fixed seed and control the clock. For instance, in testing scenarios:

// .NET 8 xUnit example
[Fact]
public void ParseInvoice_IsStable()
{
    var rng = new Random(42); // fixed seed
    var now = new DateTime(2025, 10, 09, 2, 11, 00, DateTimeKind.Utc);
    using var clock = new FixedClock(now); // your simple IClock
    var sut = new InvoiceParser(clock, rng);
    Assert.Equal(123.45m, sut.Parse(total:123.45));
}
Enter fullscreen mode Exit fullscreen mode

This step is crucial for stable testing, which allows you to accurately bisect and benchmark.

2: Create a Minimal Reproduction

Reduce the code to the smallest amount required to cause the problem. Copy just enough code into a basic console app or test case that replicates the error. Avoid including the whole dependency injection setup and middleware. A smaller reproduction makes it easier to identify the signal.

dotnet new console -n Repro.LoginHang
cd Repro.LoginHang
Enter fullscreen mode Exit fullscreen mode
// Program.cs
using System.Net.Http;

var client = new HttpClient(new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromMinutes(5)
});

for (int i = 0; i < 100; i++)
{
    var t = client.GetAsync(https://example.test/login).GetAwaiter().GetResult(); // sync-over-async trap
    Console.WriteLine((int)t.StatusCode);
}
Enter fullscreen mode Exit fullscreen mode

This short code may reveal deadlocks or problems of running out of the pool. Then you can start the debugging easier.

3: Use Specific Logging and Counters

Rather than adding too many logs, focus on adding useful ones. Place logs near the code that might be slow or risky, and include correlation IDs and timing information.

using var activity = MyActivitySource.StartActivity(Auth.ExchangeCode);
var sw = ValueStopwatch.StartNew();
// call the dependency
_logger.LogInformation(auth.exchange started {CorrelationId}, cid);
// ...
_logger.LogInformation(auth.exchange done {CorrelationId} {ElapsedMs}, cid, sw.GetElapsedTime().TotalMilliseconds);
Enter fullscreen mode Exit fullscreen mode

Track live counters to detect leaks or starvation by streaming:

# CPU, GC, threadpool, exceptions in real time
dotnet-counters monitor System.Runtime --process-id <pid>
Enter fullscreen mode Exit fullscreen mode

Look for patterns such as:

  • Thread pool queue length increasing without recovery
  • GC heap size growing continuously
  • Exception rates peaking around the time of the error

4: Use git bisect to Find the Problematic Commit

Instead of guessing, use the repository's history to find the change that introduced the fault.

git bisect start
git bisect bad HEAD
git bisect good v1.18.0   # pick a known-good tag or commit
# For each step:
# 1) build and run the minimal repro script
# 2) mark good/bad
git bisect bad   # or git bisect good
Enter fullscreen mode Exit fullscreen mode

This procedure typically takes seven to ten steps. Once it's complete, the offending commit can be inspected. Analyze the changes and confirm the result.

5: Write a Test That Fails Before Fixing

Create a test that captures the problematic behavior. This ensures that the problem won't occur again.

[Fact]
public async Task Login_Timeouts_WhenTokenSwallowed()
{
    using var server = TestServerFactory.Create();
    var client = server.CreateClient();
    var sw = Stopwatch.StartNew();
    await Assert.ThrowsAsync<timeoutexception>(() =&gt; client.GetAsync(/login));
    Assert.True(sw.Elapsed &lt; TimeSpan.FromSeconds(5)); // fails today
}
Enter fullscreen mode Exit fullscreen mode

Now fix the code. Resolve sync-over-async issues, handle exceptions correctly, and set appropriate timeouts.

// Before
var token = _provider.Get().Result; // blocks UI or threadpool

// After
var token = await _provider.GetAsync(); // flows correctly with await
Enter fullscreen mode Exit fullscreen mode

Run the test to see if it passes. Add a brief comment explaining the edge case.

Top comments (0)