Debugging common C# exceptions in modern web applications
Every full-stack developer who has shipped a C# web application has lived through the moment when a red error page replaces what should have been a perfectly normal page load. Exceptions are unavoidable in any non-trivial system, but the difference between a developer who spends three days hunting a NullReferenceException and one who fixes it in twenty minutes usually comes down to a few well-practised habits. In busy Australian engineering teams where release windows are tight and on-call rotations can drag you out of bed at 2am AEST, those habits quickly become career-defining.
The .NET runtime gives you far more information than most developers realise, even when an exception looks like a wall of opaque text. Stack traces, inner exceptions, and structured logs each carry clues. Once you learn to read them in the right order, the wall becomes a map. The goal of this piece is to walk through the exceptions I see most often in ASP.NET web applications, the patterns that usually cause them, and the pragmatic steps I take to track them down.
We won't pretend exceptions disappear once you master them. They will keep appearing in code you didn't write, in third-party libraries, and in production environments that behave differently from your local machine. What you can do is reduce the time between an exception being thrown and you understanding why, so the next red page in your logs feels like a small annoyance rather than an emergency.
NullReferenceException and the silent crashes it causes
NullReferenceException remains the most thrown exception in .NET, decades after the language first introduced nullable types. In ASP.NET, the offender is usually one of three things: a service that was never registered in the dependency injection container, a property on a deserialised model that came back null, or a chain of dot-operations where one link is missing. The default message — "Object reference not set to an instance of an object" — does not tell you which object, but the stack trace usually points straight at the line that tried to dereference it.
The first move I make is to look carefully at the call site. Modern C# with nullable reference types enabled catches most of these issues at compile time, but plenty of Australian codebases are still running on .NET Framework 4.x or pre-C# 8 code that ships without the annotations. If you maintain that kind of codebase, the cheapest upgrade you can make is to turn on <Nullable>enable</Nullable> and start fixing the warnings one file at a time. It pays off within a week.
When you cannot reproduce locally, structured logging saves you. Libraries like Serilog or NLog let you emit exception details with a correlation ID that ties a single request together across services. In production systems I have worked on, ranging from a Sydney fintech to a Brisbane government portal, we made the correlation ID mandatory in middleware and stamped it on every log line, every outgoing HTTP call, and every database command. Without it, debugging a NullReferenceException that fires once in a thousand requests is mostly guesswork.
Defensive coding helps prevent the exception from being thrown in the first place. Null-conditional operators such as user?.Profile?.Email short-circuit before they touch a missing property, and the null-coalescing operator ?? lets you supply a sensible fallback. For method parameters, ArgumentNullException.ThrowIfNull() is a clean way to fail fast at the boundary instead of letting nulls wander deep into your logic.
When the database throws a tantrum
SqlException, TimeoutException, and the various flavours of DbUpdateException from Entity Framework Core cover most database-side failures. Latency to cloud databases hosted in Australian Azure regions (Sydney or Melbourne) is generally low, but cross-region reads to overseas replicas, queries that hit a stretched Always On availability group, or a noisy neighbour on a shared SQL tier can stretch query times well past their usual baseline. A connection that takes thirty seconds to fail often looks like a hang in the application layer rather than a database issue.
The first thing to verify is the connection string itself. A swapped environment variable, a missing credential after a secret rotation in Azure Key Vault, or a typo in the Initial Catalog will produce login failures that masquerade as more dramatic problems. Local developers based in Perth frequently forget that even a connection to a Sydney-region database still traverses the continent, and any local network blip will surface as a SqlException with a generic message.
EF Core offers a developer-mode flag that surfaces the underlying SQL and parameter values for every command. Enable it with EnableSensitiveDataLogging() and EnableDetailedErrors() on your DbContextOptionsBuilder. In production, switch both off but keep a structured log of the inner exception. The wrapped SqlException carries the error number, severity, and state, all of which are searchable online against Microsoft's documentation.
For genuine timeouts, never assume the application is at fault. A missing index in SQL Server can turn a sub-second query into a thirty-second one, and adding logging will not fix the index. Tools like SQL Server's Query Store, the Database Engine Tuning Advisor, or paid options such as SolarWinds Database Performance Analyzer identify slow queries without adding tracing to your application. If you operate in Australia and must comply with the Notifiable Data Breaches scheme, detailed database query logs also help reconstruct what data was touched during an incident.
HTTP and API request errors that catch teams off guard
- HttpRequestException usually wraps a DNS failure, a connection refused, or a TLS handshake error. Intermittent drops to overseas APIs happen, especially on consumer NBN connections. Implement retry policies with exponential backoff and jitter using Polly, and consider caching the most common responses to soften the impact of an upstream outage.
- TaskCanceledException surfaces when an HttpClient call exceeds its timeout. Distinguish between user-initiated cancellation (the browser navigated away) and a real timeout by inspecting the CancellationToken. Logging the difference matters because one is a feature and the other is an incident.
- BadHttpRequestException, raised by ASP.NET Core itself, points to malformed requests, oversized payloads, or invalid headers. Watch the status code that accompanies it: 400 means a client fault, 413 means the request body exceeded the configured limit, and 415 means the content type was wrong.
- 401 versus 403: a missing token produces 401, an insufficient scope produces 403. In mixed environments with both JWT bearer authentication and cookie-based auth, misconfigured authentication schemes produce confusing redirect loops that often log as 401 even though the user is signed in.
- SerializationException from System.Text.Json usually means a casing mismatch, an unknown enum value, or a circular reference that you forgot to ignore. Configure JsonSerializerOptions explicitly rather than relying on defaults, and consider source generators for AOT-friendly pipelines.
Async and concurrency pitfalls worth knowing
- ConfigureAwait(false) is no longer strictly necessary in ASP.NET Core because the classic SynchronizationContext pitfall disappeared, but it is still good practice inside reusable library code. Adopting it as a habit means your helper classes keep working the day someone lifts them into a WPF or WinUI host.
- Async void is dangerous outside event handlers. Unhandled exceptions inside an async void method tear down the process because they cannot be caught with the usual try/catch blocks and never reach the Task that awaits them. Stick to async Task for almost everything; reserve async void for top-level event handlers only.
- Captured loop variables caused countless bugs before C# 5 introduced foreach semantics that capture per iteration. If you work on a legacy codebase that still uses for loops with lambdas, copy the variable into a local inside the loop body before capturing it. The compiler will not save you here.
- Lock contention on singleton services often surfaces as TimeoutException from SemaphoreSlim or as sluggish response times under load. Profile with dotnet-counters and dotnet-trace before adding more locks, because the wrong fix can make things worse. Sometimes the right answer is to remove the lock entirely by switching to an immutable data structure.
- Cancellation propagation is one of the most overlooked details in modern C#. Every public async method should accept a CancellationToken and pass it to every downstream call. Background services that ignore the host's shutdown token will keep churning through queued work long after the orchestrator asked them to stop. During a Kubernetes rolling update in an AKS cluster in Sydney, this can mean graceful shutdowns that take five minutes longer than they should.
Reading stack traces like a detective
A stack trace tells a story, but only if you read it bottom-up. The deepest frame is where the exception was actually thrown; everything above is the path the call took to arrive there. When I am working through a tricky one with a colleague over a flat white in a Melbourne laneway café, we usually sketch the call chain on a napkin before touching the keyboard, because the act of drawing it forces us to commit to an order of events.
Filter by exception type first. If the message mentions "Object reference not set to an instance of an object", you know it is a NullReferenceException; you can ignore unrelated frames and focus on the lines inside your own assemblies. For third-party libraries, the stack trace tells you exactly which method to read in the source code. GitHub repositories often have the same line numbers as the compiled DLLs, especially for libraries that publish symbols to nuget.org.
Use ExceptionDispatchInfo.Capture(ex).Throw() when re-throwing to preserve the original stack trace. A bare throw ex; resets it and hides where the error actually originated — a long-standing gotcha for developers who learned C# in the early 2000s. This single habit has saved me hours in more than one production incident at a Sydney-based bank, where every minute of downtime has a real cost attached to it.
Visual Studio, JetBrains Rider, and even VS Code with the C# extension offer excellent exception breakpoints. Set them on the specific exception types you are investigating, and the debugger will pause the moment they fire, even inside framework code. For live production diagnostics, Application Insights (available in the Australian Azure regions) and Seq give you searchable exception histories with full request context, which is invaluable when a customer in Adelaide reports a bug that nobody in Sydney or Melbourne can reproduce.
If you have wrestled with one of these exceptions this week, send me the story through the contact page or open a thread on the GitHub repo linked in the footer. I publish follow-up posts based on real reader questions, and there is a fair chance your particular flavour of NullReferenceException will become the next debugging walk-through.