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.
throw 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
// 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 runningWhen 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.
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.
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 4Asking 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.
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