Managing Errors with Exceptions

Managing Errors with Exceptions

All nontrivial software programs are subject to errors. These errors can come in many forms, such as coding errors, runtime errors, or errors in cooperating systems. Regardless of the form that an error takes or where it’s detected, each of these errors must be handled in some way. In C#, all errors are considered exceptional events that are managed by an exception handling mechanism.

Exception handling provides a robust way to handle errors, transferring control and rich error information when an error occurs. When an exception is raised, control is transferred to an appropriate exception handler, which is responsible for recovering from the error condition.

Code that uses exceptions for error handling tends to be clearer and more robust than code that uses other error handling methods. Consider the following code fragment that uses return values to handle errors:

bool RemoveFromAccount(string AccountId, decimal Amount)
{
    bool Exists = VerifyAccountId(AccountId);
    if(Exists)
    {
        bool CanWithdraw = CheckAvailability(AccountId, Amount);
        if(CanWithdraw)
        {
            return Withdraw(AccountId, Amount);
        }
    }
    return false;
}

Code that depends on return values for error detection has the following problems:

  • In cases in which a bool value is returned, detailed information about the error must be provided through additional error parameters or by calling additional error handling functions. This is the approach taken by much of the Win32 API.

  • Returning an error or a success code requires each programmer to properly handle the error code. The error code can be easily ignored or interpreted incorrectly, and an error condition might be missed or misinterpreted as a result.

  • Error codes are difficult to extend or update. Introducing a new error code might require updates throughout your source code.

In C#, error information is passed with exceptions. Rather than return a simple error code or a bool value, a method that fails is expected to raise an exception. The exception is a rich object that contains information about the failure condition.

Handling Exceptions

To handle an exception, you must protect a block of code using a try block and provide at least one catch block to handle the exception, as shown here:

try
{
    Dog d;
    // Attempt to call through a null reference.
    d.ChaseCars();
    
    

}
catch
{
    // Handle any exception.
    
    

}

When an exception is raised, or thrown, within a try block, an exception object of the proper type is created and a search begins for an appropriate catch clause. In the preceding example, the catch clause doesn’t specify an exception type and will catch any type of exception. Program execution is transferred immediately to the catch clause, which can perform one of these actions:

  • Handle the exception and continue execution after the catch clause

  • Rethrow the current exception

To catch only a specific exception, the catch clause supplies a parameter that serves as a filter, specifying the exception or exceptions that it’s capable of handling, as shown here:

try
{
    Dog d;
    // Attempt to call through a null reference.
    d.ChaseCars();
    
    

}
catch(NullReferenceException ex)
{
    // Handle only null reference exceptions.
    
    

}

If the catch clause can recover from the error that caused the exception, execution can continue after the catch clause. If the catch clause can’t recover, it should rethrow the exception so that another exception handler can try to recover from the error.

To rethrow an exception, the throw keyword is used with the exception object passed to the catch clause, as in the following example:

try
{
    Dog d;
    // Attempt to call through a null reference.
    d.ChaseCars();
}
catch(NullReferenceException ex)
{
    // (ex);
Log error information.
    LogError (ex);
    // Rethrow the exception.
    throw(ex);
}

To rethrow the current exception, the throw keyword can be used without specifying an exception object, as shown here:

catch(NullReferenceException ex)
{
    
    

    // Rethrow the current exception.
    throw;
}

In a general catch block, throw must be used by itself, as there is no exception object to be rethrown:

catch
{
    
    

    // Rethrow the current exception.
    throw;
}

If an exception is rethrown, a search is performed for the next catch clause capable of handling the exception. The call stack is examined to determine the sequence of methods that have participated in calling the current method. Each method in the call stack can potentially handle the exception, with the exception being offered first to the immediate calling method if an acceptable catch clause exists. This sequence continues until the exception is handled, or until all methods in the call stack have had an opportunity to handle the exception. If no handler for the exception is found, the thread is terminated due to the unhandled exception.

Using Exception Information

A reasonable description of the exception can be retrieved from any of the .NET Framework exceptions by calling the exception object’s ToString method, as shown here:

catch(InvalidCastException badCast)
{
    Console.WriteLine(badCast.ToString());
}

Other information is available from exception objects, as described here:

  • StackTrace  Returns a string representation of a stack trace that was captured when the exception was thrown

  • InnerException  Returns an additional exception that might have been saved during exception processing

  • Message  Returns a localized string that describes the exception

  • HelpLink  Returns a URL or Universal Resource Name (URN) to more information about the exception

If you create your own exception types that are exposed to others, you should provide at least this information. By providing the same information included with .NET Framework exceptions, your exceptions will be much more usable.

Providing Multiple Exception Handlers

If code is written in such a way that more than one type of exception might be thrown, multiple catch clauses can be appended, with each clause evaluated sequentially until a match for the exception object is found. At most, one of the catch clauses can be a general clause with no exception type specified, as shown here:

try
{
    Dog d = (Dog)anAnimal;
}
catch(NullReferenceException badRef)
{
    // Handle null references.
    
    

}
catch(InvalidCastException badCast)
{
    // Handle a bad cast.
    
    

}
catch
{
    // Handle any remaining type of exception.
    
    

}

The catch clauses are evaluated sequentially, which means that the more general catch clauses must be placed after the more specific catch clauses. The Visual C# compiler enforces this behavior and won’t compile poorly formed catch clauses such as this:

try
{
    Dog d = (Dog)anAnimal;
}
catch(Exception ex)
{
    // Handle any exception.
}
catch(InvalidCastException badCast)
{
    // Handle a bad cast.
}

In this example, the first catch clause will handle all exceptions, with no exceptions passed to the second clause, so an invalid cast exception won’t receive the special handling you expected. Because this is obviously an error, the compiler rejects this type of construction.

It’s also possible to nest exception handlers, with try-catch blocks located within an enclosing try-catch block, as shown here:

try
{
    Animal anAnimal = GetNextAnimal();
    anAnimal.Eat();
    try
    {
        Dog d = (Dog)anAnimal;
        d.ChaseCars();
    }
    catch(InvalidCastException badCast)
    {
        // Handle bad casts.
    
    

    }
}
catch
{
    // General exception handling
    
    

}

When an exception is thrown, it’s first offered to the most immediate set of catch clauses. If the exception isn’t handled or is rethrown, it’s offered to the enclosing try-catch block.

Guaranteeing Code Execution

When you’re creating a method, sometimes there are tasks that must be executed before the method returns to the caller. Often this code frees resources or performs other actions that must occur even in the presence of exceptions or other errors. C# allows you to place this code into a finally clause, which is guaranteed to be executed before the method returns, as shown here:

try
{
    Horse silver = GetHorseFromBarn();
    silver.Ride();
}
catch
{
    
    

}
finally
{
    silver.Groom();
}

In this example, the finally clause is executed after the exception is thrown. A finally clause is also executed if a return, goto, continue, or break statement attempts to transfer control out of the try clause.

As you can see, you can have a finally clause and catch clauses attached to the same try clause. If a catch clause rethrows an exception, control is first passed to the finally clause before it’s transferred out of the method.

.NET Framework Exceptions

The .NET Framework includes a large number of exception classes. Although all exceptions are eventually derived from the Exception class in the System namespace, the design pattern used in .NET is to include exceptions in the namespaces that generate the exceptions. For example, exceptions that are thrown by IO classes are located in the System.IO namespace.

Exceptions in the .NET Framework are rarely directly inherited from the System.Exception class. Instead, they’re clustered into categories, with a common subclass that enables clients to group similar exceptions together. For example, all IO exceptions are subclasses of the System.IO.IOException class, which is derived from System.Exception. This hierarchical relationship enables clients to write exception handlers that handle IO-specific errors in a single catch block without the need to write separate handlers for each exception, as shown here:

catch(IOException ioError)
{
    // Handle all IO exceptions here.
    
    

}

When developing your own exception classes, consider following this pattern and deriving your classes from one of the many subclasses rather than deriving directly from the System.Exception class. Doing so will make your exceptions more usable by client code and might enable clients to handle exceptions thrown by your classes without requiring any new code.



Part III: Programming Windows Forms