Freigeben über


Top 5 .NET Exceptions

By Liam Westley, Application Architect at Huddle

When you use the word Exception it always sounds scary, but exceptions in .NET are far from scary and are much more useful than the old days of returning error codes and error strings.  There’s no excuse for your methods to provide return values which hide the details of why something went wrong.

The myth that exceptions are slow to throw and hog memory have long been debunked.  If they aren’t used as a standard control flow (see item 2 below) then they will only occur rarely and provide vital information on the state of your application.

Here are my top five tips for handling .NET Exceptions.

1. Maintain your stack trace

When you first start programming in .NET you might think that you catch the exception, try some stuff but rethrow that exception if you couldn’t handle it.  Or, you may have defined your own exception class.  It might have looked like the following code.

In both these cases, you will find that the stack trace provided with your exception will go no further down the call stack than the method PartialStackTrace .

 private double PartialStackTrace()
{
  try
  {
       return DivideANumber(1, 0);
  }
  catch (Exception ex)
  {
       throw ex;
  }
}

OR

private double PartialStackTrace()
{
  try
  {
       return DivideANumber(1, 0);
  }
  catch (Exception ex)
  {
       throw new MyAppException(“Ooops!”);
  }
}

You really would like a full stack trace, that reveals that the problem exists in DivideANumber (or further down the call stack).

If you are just re-throwing the exception use throw by itself, and the full stack trace is preserved.

If you define your own exception class, provide the exception you just caught as the InnerException in the constructor.  Job done!

 private double FullStackTrace()
{
  try
  {
       return DivideANumber(1, 0);
  }
  catch (Exception ex)
  {
       throw;
  }
}

OR

private double FullStackTrace()
{
  try
  {
       return DivideANumber(1, 0);
  }
  catch (Exception ex)
  {
       throw new MyAppException(“Ooops!”, ex);
  }
}

See also: /en-us/visualstudio/code-quality/ca2200-rethrow-to-preserve-stack-details.

2. Keep exceptions exceptional…

This sounds a bit trite, but sometimes you won’t need exceptions as you know there will be times that you will receive data or be in a situation where an exception might occur, and you know how to handle it.

As an example, it is trivial to verify that a key exists in a dictionary prior to attempting to access a value, and save having to wrap dictionary access in a try … catch block.

 var alphabet = new Dictionary<int, string>() { { 1, "A" }, { 2, "B" } };

// throws KeyNotFoundException

try
{
    Console.WriteLine($"27th letter of alphabet {alphabet[27]}");
}
catch (KeyNotFoundException kex)
{
    Console.WriteLine("27th letter of alphabet : <not present>");
}

// check first, no try ... catch required
var letter = alphabet.ContainsKey(27) ? alphabet[27] : "<not present>";
Console.WriteLine($"27th letter of alphabet : { letter }");k

Similarly, you might want to check for the existence of a file with the same name before an attempt to copying a new file.  This could be gracefully handled by changing the resulting file name and providing the new file name back as a result.

3. Avoid generic, catch-all exception handling

Not only should you not throw exceptions for conditions that are expected to occur regularly, you should also always aim to catch specific exceptions, rather than generic exceptions.  This has become finer grained in C# 6 as it supports filtering using catch ... when and supplying a suitable predicate.

 // handle specific exception differently

try
{
    double result = DivideByZeroCheckForSpecificException();
    Console.WriteLine($"Divide by zero results : {result}\n");
}
catch (DivideByZeroException dex)
{
    Console.WriteLine("Divide by zero results : <cannot divide by 0>\n");
}
catch (Exception ex)
{
    Console.WriteLine("Unknown exception : \n\n" + ex.ToString() + "\n");
}

// handle exception using a catch ... when predicate 

try
{
    double result = DivideByZeroCheckExceptionPredicate();
    Console.WriteLine($"Divide by zero results : {result}");
}
catch (Exception ex) when (ex.InnerException != null)
{
    Console.WriteLine("An InnerException exists : \n\n" + ex.ToString() + "\n");
}

4. Async and exception handling

The world of async has given us a few more things to think about regarding exceptions.  The first is that you will be expected to handle the AggregateException much more often than you may have done previously.  This wrapper could be wrapping several exceptions, or just a single exception.  You have to examine the InnerExceptions property to see if there are underlying exceptions which you could handle.

It should also be noted that methods which are decorated with async will not throw exceptions unless you await them.  This can be easy to miss when creating unit tests with mocks and stubs, and can lead to much frustration as tests suddenly fail due to exceptions not being thrown and handled as expected.

 internal static async Task ThrowAnException()
{
    throw new Exception("Throwing from an async decorated method.");
}
private static async Task TestAsyncExceptions()
{

  // No exception will be thrown, we omitted await

  try
  {
    ThrowAnException();
    Console.WriteLine("No exception thrown.");
  }
  catch (Exception ex)
  {
    Console.WriteLine("Exception thrown : \n\n" + ex.ToString() + "\n");
  }

  // Exception will be thrown because we have used await

  try
  {
    await ThrowAnException();
    Console.WriteLine("No exception thrown.");
  }
  catch (Exception ex)
  {
    Console.WriteLine("Exception thrown : \n\n" + ex.ToString() + "\n");
  } 
}

5. Don’t rely on exception message content, it could be localised

Even when you do capture on specific exception types, you might find that the exception is ambiguous and doesn’t really inform you of the underlying issue.  It can be tempting to check for a given exception message to really confirm that the exception is something you think you can handle.

 try
{  
  getDocument = DocStore.SingleOrDefault(d => d.SelfLink = doc.SelfLink); 
}
catch (System.InvalidOperationException ex)
{    
  if (ex.Message == “Sequence contains more than one matching element”)  
  {     
    CleanUpDuplicates(doc.SelfLink);  
  }  
  else  
  {    
   throw;  
  }
}

The above example works fine if the system on which your software runs uses a English language locale, but if it were, say based in Norway the text ‘Sequence contains more than one matching element” becomes

Sekvensen inneholder mer enn ett samsvarende element

A good solution to this issue is to handle the specific exception but validate that the issue really is more than one item being found.  By counting how many items might be present that match the predicate, we only call CleanUpDuplicates if we actually find duplicates, otherwise we can happily throw as the exception is unexpected.

Summary

So that is my top five tips for handling .NET Exceptions.  I hope you can embrace exceptions and the rich data they provide when trying to debug your applications. With libraries such as Polly utilising your exceptions to implement patterns such as Retry and Circuit Breaker, there’s even more reasons to use exceptions in your code to save time and effort.

Biography

Liam Westley is an ex Microsoft MVP and Application Architect at Huddle where he works with some of the best .NET developers and UX designers to deliver world class collaboration software. He quite likes working just on the edge of The City as there is some fantastic food and coffee to be had within a few minutes walk.

Liam has worked for chellomedia, GMTV, BSkyB, QVC UK and chellomedia. In his time he created the first in house weather system for Sky News using Visual Basic 1.0, acted as architect for two general election systems, project managed the launch of the GMTV web site, was key to delivering the first interactive television chat service in the UK for BSkyB and helped launch the first live shopping channels in the Netherlands.

Liam has a blog at https://blog.liamwestley.co.uk and https://medium.com/\@liam.westley and can be found on twitter at @westleyl.

Comments

  • Anonymous
    July 25, 2017
    In #1 you say "If you are just re-throwing the exception use throw by itself, and the full stack trace is preserved."NO! If you are just re-throwing the exception-- DON'T CATCH it at all. Just let it bubble up to the next catch.
    • Anonymous
      July 31, 2017
      James, I think my attempt to keep the code samples as simple as possible is an issue here - I was more thinking of the case where you might perform some sort of compensation action, or log with some local state, within the Catch block and then have to throw to bubble the exception up the stack.You are quite right - if you are only going to re-throw, then don't, just let it bubble up on it's own is always a better approach.
  • Anonymous
    July 25, 2017
    The advice to "Avoid generic, catch-all exception handling" can only be taken so far. Have you looked at all the possible exceptions a simple call to most .NET Framework methods can generate? Code could in those cases be littered with specific catches, most of which you'll want to take the same general error handling action for. The discovery process for catching all the correct possible throws is quite time consuming, even when the doc is good (and correct). I believe we should always qualify this advice - it has its time and place. It should (almost) always be followed in your own code with your own exceptions. When calling APIs with a known and limited set of exceptions for which you want to handle an exception in a specific way - yep then too.
    • Anonymous
      July 31, 2017
      I suppose the qualified explanation is that the aim is to catch the specific exceptions for which compensating actions make sense, while allowing generic exceptions to possibly bubble up through to a higher level.There is a danger in catching ALL exceptions, some of which might be unforeseen (as you say, the docs are not always complete) and for which a standard compensating action may cause more harm than good.So yes, it is situation dependent, but the more you handle known exceptions the greater the chance that any handling or compensating actions will be appropriate to the even which occurred. So they certainly do have a time and a place where this is appropriate.
  • Anonymous
    July 31, 2017
    "Top 5 .NET Exceptions", no it's not. It's a post with a top 5 tips for exception handling