Clean Assert Wrappers

Pope Kim Aug 26, 2025

When working in C#, you often use debugging APIs like Debug.Assert() or Debug.Fail(). But if you call them directly across the whole project, it quickly becomes inconvenient. That's why many developers create a wrapper function and use it globally.

For example:

DBG_CHECK(x != null, "x is null!");

Why bother with a wrapper?

  • If you import DBG_CHECK() globally via using static, typing becomes easier.
  • Since the name is in ALL CAPS, it stands out in the code. → It's immediately obvious that this is a debug-only utility, not business logic, so you can skim past it easily.
  • You can also sneak in extra enforcement logic inside the wrapper.

In other words, wrapping repeated Debug.Assert() calls makes them cleaner, easier to use, and more visible.


Why use [Conditional("DEBUG")]

This wrapper should, of course, only run in DEBUG mode. In C#, attaching the [Conditional("DEBUG")] attribute tells the compiler to remove the call entirely in release builds.

[Conditional("DEBUG")]
public static void DBG_CHECK(bool condition, string? message = null)
{
    if (!condition)
    {
        Debug.Fail(message);
    }
}
  • In release builds, not even IL code remains.
  • You no longer need to wrap everything in #if DEBUG ... #endif.
  • From a performance and security perspective, you must never leave asserts in release builds.

The annoying part: stack trace

The problem is that when Debug.Assert() hits inside the wrapper, the top of the stack is always your DBG_CHECK() wrapper function.

For example:

void Foo()
{
    Bar(null);
}

void Bar(object? arg)
{
    DBG_CHECK(arg != null, "arg is null");
}

If arg is null, the stack looks like this:

DBG_CHECK()
Bar()
Foo()

What you really care about is Bar(), but the debugger always stops inside DBG_CHECK(). So you have to "Step Out" every time to get to the actual caller. Pretty annoying.

The fix: [DebuggerHidden]

This is where [DebuggerHidden] comes to the rescue.

[Conditional("DEBUG")]
[DebuggerHidden]
public static void DBG_CHECK(bool condition, string? message = null)
{
    if (!condition)
    {
        Debug.Fail(message);
    }
}
  • DebuggerHidden tells the debugger to hide this function's frame.
  • If a break happens inside it, the debugger jumps directly to the caller.
  • Works flawlessly in Visual Studio

Example in action

In the Bar() example above, when DBG_CHECK() fails, the debugger now shows:

Bar()
Foo()

The DBG_CHECK() frame is gone, so you can start debugging exactly at the caller site.


Extra details

  • DebuggerHidden is supported from .NET Framework 2.0 onward, including .NET Core and .NET 5–9.
  • Verified in Visual Studio 2019 and 2022. (JetBrains Rider shows similar behavior but may vary slightly.)
  • Caveat: you cannot set breakpoints inside a method marked [DebuggerHidden].
    But since this is just an assert wrapper, that's not a problem.

Wrap-up

  • Wrapping Debug.Assert() with something like DBG_CHECK() makes it easier to type, more visible, and cleaner.
  • [Conditional("DEBUG")] ensures it never appears in release builds.
  • The downside is that the wrapper clutters the call stack, but adding [DebuggerHidden] fixes that by shifting focus to the caller.
  • In Visual Studio, the experience is clean and seamless.

👉 Bottom line: When writing assert wrappers, the Conditional("DEBUG") + DebuggerHidden combo is basically a must-have.