Exceptions

What an exception is, what throwing one does to the running program, and how to read a stack trace to find the line that failed.

Syntaxthrow new InvalidOperationException("what went wrong");

Some operations cannot finish. A file is missing. A number is too large for the type holding it. A method is handed a value it cannot use. An exception is an object the runtime creates to describe such a failure, and throw is the statement that hands it over to whoever called you.

What throwing does

Running code keeps a call stack: the list of methods that are part-way through, the newest one first. When a method throws, the rest of that method is abandoned. The runtime then searches outwards through the stack for a catch block that handles this type of exception. Every method it passes on the way is abandoned too. If nothing handles it, the program prints the exception and stops.

Reading a stack trace

Every exception carries three things worth looking at. Message is the human-readable description. GetType() returns the exception's type, which is how you know what kind of failure it was. StackTrace is text listing the methods that were on the stack at the moment of the throw. Read it from the top: the first line is where it broke, and each line below it is the caller of the line above.

What trips people up

The top line of a trace is often inside .NET itself rather than your code. Scroll down to the first file of yours. Also check InnerException: when one failure causes another, the original is stored there, and the message printed first belongs to the outer one.

Example

Example · csharp
// A method that refuses input it cannot use, and what the refusal looks like.
int Half(int value)
{
    if (value % 2 != 0)
    {
        // Stop here. This method cannot do what the caller asked.
        throw new ArgumentException("value must be even", nameof(value));
    }

    return value / 2;
}

Console.WriteLine($"half of 10 is {Half(10)}");

try
{
    // Half(7) throws, so this WriteLine never prints anything.
    Console.WriteLine($"half of 7 is {Half(7)}");
}
catch (ArgumentException ex)
{
    Console.WriteLine($"caught {ex.GetType().Name}");
    Console.WriteLine($"message: {ex.Message}");
    Console.WriteLine($"parameter: {ex.ParamName}");
}

Console.WriteLine("still running");

// Output:
// half of 10 is 5
// caught ArgumentException
// message: value must be even (Parameter 'value')
// parameter: value
// still running

When to use it

  • A request failed in production and the logged stack trace is all you have to find the line.
  • A NullReferenceException points into library code and you need your own frame in the trace.
  • Explaining why the program stopped at line 30 instead of printing line 31.

More examples

The original failure lives in InnerException

The second argument to most exception constructors is the exception that caused this one. It is stored in InnerException and printed by the runtime after a ---> marker. Reach for this when you want to report a failure in your own words without throwing away the evidence underneath.

Example · csharp
try
{
    try
    {
        int[] prices = [10, 20];
        Console.WriteLine(prices[5]);   // only indexes 0 and 1 exist
    }
    catch (IndexOutOfRangeException ex)
    {
        // Wrap a low-level failure in one that means something to the caller,
        // passing the original as the second argument so it is not lost.
        throw new InvalidOperationException("price table is too short", ex);
    }
}
catch (InvalidOperationException ex)
{
    Console.WriteLine(ex.Message);
    Console.WriteLine(ex.InnerException?.Message);
}

// Output:
// price table is too short
// Index was outside the bounds of the array.

Reading a real trace

Line 1 is the exception type and message. The ---> line is the inner exception, and the frames above the "End of inner exception" marker belong to it, so Prices.cs line 12 is where it actually broke. Below the marker is the outer exception's own stack: Load called Lookup, and Main called Load. The <Main>$ name is what the compiler calls the method generated from top-level statements.

Example · text
Unhandled exception. System.InvalidOperationException: price table is too short
 ---> System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at Prices.Lookup(Int32 index) in /app/Prices.cs:line 12
   --- End of inner exception stack trace ---
   at Prices.Load() in /app/Prices.cs:line 20
   at Program.<Main>$(String[] args) in /app/Program.cs:line 4

Asking an exception what it is

The exception's type is the part you write code against, and the message is only for humans to read. Use GetType().FullName when you are writing something into a log that another person will search, and never branch on the text of Message.

Example · csharp
object boxed = "not a number";

try
{
    // The cast compiles, because the compiler only knows boxed is an object.
    // At run time the value is a string, so the cast fails.
    int value = (int)boxed;
    Console.WriteLine(value);
}
catch (InvalidCastException ex)
{
    // FullName includes the namespace; Name would print InvalidCastException.
    Console.WriteLine(ex.GetType().FullName);
}

// Output:
// System.InvalidCastException

Discussion

  • Be the first to comment on this lesson.