A well-designed app handles exceptions and errors to prevent app crashes. Below are some of the best practices:
1. Use try/catch/finally blocks to recover from errors or release resources
Use try/catch blocks around code that can potentially generate an exception and your code can recover from that exception.
2. Throw specific exceptions to make it easy for handling.
In catch blocks, always order exceptions from the most derived to the least derived.
3. Avoid return statement inside Finally block as it suppress the exception being thrown out from the method.
4. Avoid exceptions, Avoid returning Null, Handle common conditions
Handle common conditions without throwing exceptions. Design classes so that exceptions can be avoided. A class can provide methods or properties that enable you to avoid making a call that would trigger an exception. For example, a FileStream class provides methods that help determine whether the end of the file has been reached. These can be used to avoid the exception that is thrown if you read past the end of the file.
It may sound obvious to avoid exceptions. But many methods that throw an exception can be avoided by defensive programming.
One of the most common exceptions is NullReferenceException. When we return null, we are essentially creating work for ourselves and foisting problems upon our callers. All it takes is one missing null check to send an application spinning out of control. In some cases, you may want to allow null but forget to check for null. Here is an example that throws a NullReferenceException:
Address a = null;
var city = a.City;
Accessing a throws an exception but play along and imagine that a is provided as a parameter. In case you want to allow a city with a null value, you can avoid the exception by using the null-conditional operator:
Address a = null;
var city = a?.City;
By appending ? when accessing a, C# automatically handles the scenario where the address is null. In this case, the city variable will get the value null.
Do not use exceptions for the normal flow of control, if possible. Except for system failures and operations with potential race conditions, framework designers should design APIs so users can write code that does not throw exceptions. For example, you can provide a way to check preconditions before calling a member so users can write code that does not throw exceptions.
5. Throw exceptions instead of returning an error code. Exceptions ensure that failures do not go unnoticed because calling code didn't check a return code.
6. Consider the performance implications of throwing exceptions. Throw rates above 100 per second are likely to noticeably impact the performance of most applications.
7. Do document all exceptions thrown by publicly callable members because of a violation of the member contract (rather than a system failure) and treat them as part of your contract.
Exceptions that are a part of the contract should not change from one version to the next (i.e. exception type should not change, and new exceptions should not be added).
8. Place throw statements so that the stack trace will be helpful.
The stack trace begins at the statement where the exception is thrown and ends at the catch statement that catches the exception.
Catch (SpecificException specificException)
{
// .....
throw specificException;
}
Catch (SpecificException specificException)
{
// .....
throw;
}
The main difference here is that the first example re-throw the SpecificException which causes the stack trace of original exception to reset while the second example simply retain all of the details of the original exception. You almost always want to use the 2nd example.
9. Consider using exception builder methods.
It is common to throw the same exception from different places. To avoid code bloat, use helper methods that create exceptions and initialize their properties.
Also, members that throw exceptions are not getting inlined. Moving the throw statement inside the builder might allow the member to be inlined.
class FileReader
{
private string fileName;
public FileReader(string path)
{
fileName = path;
}
public byte[] Read(int bytes)
{
byte[] results = FileUtils.ReadFromFile(fileName, bytes);
if (results == null)
{
throw NewFileIOException();
}
return results;
}
FileReaderException NewFileIOException()
{
string description = "My NewFileIOException Description";
return new FileReaderException(description);
}
}
10. Restore state when methods don't complete due to exceptions
Callers should be able to assume that there are no side effects when an exception is thrown from a method. For example, if you have code that transfers money by withdrawing from one account and depositing in another account, and an exception is thrown while executing the deposit, you don't want the withdrawal to remain in effect.
public void TransferFunds(Account from, Account to, decimal amount)
{
from.Withdrawal(amount);
// If the deposit fails, the withdrawal shouldn't remain in effect.
to.Deposit(amount);
}
The method above does not directly throw any exceptions, but must be written defensively so that if the deposit operation fails, the withdrawal is reversed.
One way to handle this situation is to catch any exceptions thrown by the deposit transaction and roll back the withdrawal.
private static void TransferFunds(Account from, Account to, decimal amount)
{
string withdrawalTrxID = from.Withdrawal(amount);
try
{
to.Deposit(amount);
}
catch
{
from.RollbackTransaction(withdrawalTrxID);
throw;
}
}
This example illustrates the use of throw to re-throw the original exception, which can make it easier for callers to see the real cause of the problem without having to examine the InnerException property. An alternative is to throw a new exception and include the original exception as the inner exception
11. Log exceptions
This seem so obvious. But we can see too much code failing in the subsequent lines when using this pattern:
try
{
service.SomeCall();
}
catch
{
// Ignored
}
Logging both uncaught and caught exceptions is the least you can do for your users. Nothing is worse than users contacting your support, and you had no idea that errors had been introduced and what happened. Logging will help you with that.