One of the least fun parts of doing legacy code debugging professionally is inheriting a codebase where NullReferenceException shows up in production logs with no context beyond a stack trace and a line number, and the object that was null could have come from a dozen different call paths. I've spent real hours in exactly that situation, and the fix that actually reduced how often it happened wasn't a debugging technique — it was turning on nullable reference types and treating the warnings as real.

Nullable reference types, enabled per-project with <Nullable>enable</Nullable> or per-file with #nullable enable, change what string means versus string?. Without it, every reference type is implicitly nullable and the compiler has no opinion; with it, string means "this is never null" and the compiler will warn you at the call site if you pass or return something that might not hold that promise.

That matters most at the boundaries — the places legacy code tends to get sloppiest. A method that returns null from some code paths and a real value from others is exactly the kind of thing nullable reference types surfaces immediately as a warning, instead of leaving it as a landmine that only goes off once some particular input reaches that method.

It's not automatic and it's not a silver bullet: turning nullable reference types on in an existing large codebase produces a wave of warnings, and it's tempting to suppress them wholesale with the null-forgiving operator (!) just to make the build quiet again. Every ! is a place where you're telling the compiler "trust me," which is the same blind trust that let the original bug exist — it's worth treating each one as a real decision, not a formatting fix.

Once it's on and taken seriously, the debugging workflow changes shape: instead of finding a NullReferenceException in a log after the fact and working backward through a stack trace to figure out where the null actually came from, the compiler flags the suspect assignment at the point it happens, while you're looking at the code that caused it.

Nullable reference types have been part of C# since C# 8; Microsoft's own C# language documentation covers the full annotation syntax, including less common cases like generic type parameters and array elements, in more depth than a single article can.