Being stupid is fun: Let's create long exception messages!

2/23/2026
4 minute read

Sometimes doing arbitrary stuff feels just good! So let's do that!


Spoiler alert and disclaimer: This blog post will generate 0 value in your life. Be warned that you lose precious lifetime without getting anything back!


new string(char c, int count)

If we create a string, we are allowed to pass in an int count. So naturally, I would do something like:

string longMessage = new('X', int.MaxValue);

But we are greeted with a:

Unhandled exception. System.OverflowException: Arithmetic operation resulted in an overflow.

Well internally new string calls a method called FastAllocateString, which is defined as:

internal static extern unsafe string FastAllocateString(MethodTable *pMT, nint length);

So basically the CLR, Common Language Runtime, handles that call ... and fails. Why is that? For starters a single object is not allowed to be larger than 2 GB. And if we oversimplify string it is a char[] (again we are oversimplifying - the reality looks a bit different). A char takes two bytes. So 2 bytes * int.MaxValue is roundabout 4 GB! So that won't work. So at least we have to take new('X', int.MaxValue / 2); But hold on, there is more (empirically try it out and get greeted by the same exception): Every object has sometimes additional metadata (like List<T> has Count which takes 4 bytes and so string has also an int Length) and some headers for the runtime itself so it can keep track of objects (like MethodTable, we also have the terminating '\0', Sync Block Index - for lock and friends - and so on).

If we do:

string longMessage = new('X', int.MaxValue / 2 - 32);

Then we are looking at success! And thanks to recent development price-wise for memory, you can feel rich! Okay. Moving on: Going on to the next part:

string longMessage = new('X', int.MaxValue / 2 - 32);

throw new Exception(longMessage);

That results in:

Cannot print exception string because Exception.ToString() failed.

That makes sense. If we have a look at a basic Exception.ToString:

public override string ToString()
{
    string className = GetClassName();
    string? message = Message;
    string innerExceptionString = _innerException?.ToString() ?? "";
    string endOfInnerExceptionResource = SR.Exception_EndOfInnerExceptionStack;
    string? stackTrace = StackTrace;

    // Calculate result string length
    int length = className.Length;
    checked
    {
        if (!string.IsNullOrEmpty(message))
        {
            length += 2 + message.Length;
        }
        if (_innerException != null)
        {
            length += Environment.NewLineConst.Length + InnerExceptionPrefix.Length + innerExceptionString.Length + Environment.NewLineConst.Length + 3 + endOfInnerExceptionResource.Length;
        }
        if (stackTrace != null)
        {
            length += Environment.NewLineConst.Length + stackTrace.Length;
        }
    }

    // Create the string
    string result = string.FastAllocateString(length);
    Span<char> resultSpan = new Span<char>(ref result.GetRawStringData(), result.Length);

    // Fill it in
    Write(className, ref resultSpan);
    if (!string.IsNullOrEmpty(message))
    {
        Write(": ", ref resultSpan);
        Write(message, ref resultSpan);
    }
    if (_innerException != null)
    {
        Write(Environment.NewLineConst, ref resultSpan);
        Write(InnerExceptionPrefix, ref resultSpan);
        Write(innerExceptionString, ref resultSpan);
        Write(Environment.NewLineConst, ref resultSpan);
        Write("   ", ref resultSpan);
        Write(endOfInnerExceptionResource, ref resultSpan);
    }
    if (stackTrace != null)
    {
        Write(Environment.NewLineConst, ref resultSpan);
        Write(stackTrace, ref resultSpan);
    }
    Debug.Assert(resultSpan.Length == 0);

    // Return it
    return result;

    static void Write(string source, ref Span<char> dest)
    {
        source.CopyTo(dest);
        dest = dest.Slice(source.Length);
    }
}

It does add a lot to our string and calls still that FastAllocateString stuff - we barely could create it and ToString is overboarding us with too much stuff! Okay we have two ways out:

  • Giving it a bit of space
  • Creating our own type that doesn't do that stuff!
string longMessage = new('X', int.MaxValue / 10 - 32);

throw new MyException(longMessage);

public class MyException : Exception
{
    public MyException(string message)
    {
        Message = message;
    }

    public override string Message { get; }

    public override string ToString()
    {
        return Message;
    }
}

Now we are looking at many X on the screen! Task accomplished.

An error has occurred. This application may no longer respond until reloaded.Reload x