Examine the output of Example 16-2 carefully. You
see the code enter Main( )
, Func1( )
, Func2( )
, and the try
block. You
never see it exit the try
block,
though it does exit Func2( )
,
Func1( )
, and Main( )
. What happened?
When the exception is thrown, execution halts immediately and is
handed to the catch
block. It
never returns to the original code path. It never
gets to the line that prints the exit statement for the try
block. The catch
block handles the error, and then
execution falls through to the code following the catch
block.
If there is no exception handler at all, the stack is unwound,
returning to the calling method in search of an exception handler. This
unwinding continues until the Main( )
method is reached, and if no exception handler is found, the
default (ugly) exception handler is invoked and the program
terminates.
In this example, because there is a catch
block, the stack does not need to
unwind. The exception is handled, and the program can continue
execution. Unwinding the stack becomes a bit more clear if you move the
try
/catch
blocks up to Func1( )
, as Example 16-3 shows.
Example 16-3. Unwinding the stack by one level
using System; namespace UnwindingTheStackByOneLevel { class Tester { static void Main( ) { Console.WriteLine( "Enter Main..." ); Tester t = new Tester( ); t.Run( ); Console.WriteLine( "Exit Main..." ); } public void Run( ) { Console.WriteLine( "Enter Run..." ); Func1( ); Console.WriteLine( "Exit Run..." ); } public void Func1( ) { Console.WriteLine( "Enter Func1..." ); try { Console.WriteLine( "Entering try block..." ); Func2( ); Console.WriteLine( "Exiting try block..." ); } catch { Console.WriteLine( "Exception caught and handled!" ); } Console.WriteLine( "Exit Func1..." ); } public void Func2( ) { Console.WriteLine( "Enter Func2..." ); throw new ApplicationException( ); Console.WriteLine( "Exit Func2..." ); } } }
Now the output looks like this:
Enter Main... Enter Run... Enter Func1... Entering try block... Enter Func2... Exception caught and handled! Exit Func1... Exit Run... Exit Main...
This time the exception is not handled in Func2( )
; it is handled in Func1( )
. When Func2( )
is called, it uses Console.WriteLine( )
to display its first
milestone:
Enter Func2...
Then Func2( )
throws an
exception and execution halts. The runtime looks for a handler in
Func2( )
, but there isn’t one. Then
the stack begins to unwind, and the runtime looks for a handler in the
calling function: Func1( )
. There is
a catch
block in Func1( )
, so its code is executed. Execution
then resumes immediately following the catch
statement, printing the exit statement
for Func1( )
and then for Main( )
.
Notice that even though the exception is handled, you are now in Func1, and there is no automatic way to return to where you were in Func2.
If you’re not entirely sure why the “Exiting Try Block” statement and the “Exit Func2” statement are not printed, try putting the code into a debugger and then stepping through it.