Click here to Skip to main content
14,271,418 members

I Take Exception

Rate this:
4.97 (17 votes)
Please Sign up or sign in to vote.
4.97 (17 votes)
15 Jul 2019CPOL
An Alice in Wonderland Trip Down the Rabbit Hole of Exception Handling

Contents

Introduction

Exception handling is not trivial. Although a try-catch block looks simple, what you do with an exception is not. The impetus for this article is based on what I wanted to with exceptions in an AWS Lambda function, but there's nothing specific to AWS Lambda here. The advanced developer should also realize that there really isn't anything new here, all I'm doing is packaging up some concepts into a single article and making heavy use of static classes, extension methods, generic parameters, explicit parameters, and conditional continuation operators. Also, I'm not looking at the more complicated issues of catching exceptions in threads or tasks. If you're interested in that topic, take a gander at my article A Concise Overview of Threads.

So You've Caught an Exception, Now What?

I categorize exceptions into two categories:

  1. Exceptions that my own code throws
  2. Exceptions that the framework or third party library throws
Test

In either case, something somewhere has to catch the exception. Let's take a look at a simple exception test jig:

using System;

namespace ITakeException
{
  class Program
  {
    static void Main(string[] args)
    {
      try
      {
        SayHi("Hi");
      }
      catch (Exception ex)
      {
        Console.WriteLine(ex);
      }
    }
  }

  static void SayHi(string msg)
  {
    throw new Exception("oops", new Exception("My Inner Self-Exception"));
  }
}

The above implementation is quite simple -- It just writes the exception to the console:

Image 1

Improving Logging

Typical logging (to a file, cloud logging app, etc.) doesn't look any better. This is really neither very human nor machine readable. Sure, you can write a program to parse it, but really, that's gross. What if instead you provided a more machine readable form of the exception, so your parsing can focus on identifying problems rather than parsing what sort of looks like a human-readable message, but really isn't? 

The StackTrace and StackFrame Classes

Enter the StackTrace and StackFrameData classes in System.Diagonistics:

catch (Exception ex)
{
  var st = new StackTrace(ex, true);

  foreach (var frame in st.GetFrames())
  {
    var sfd = new StackFrameData(frame);
    Console.WriteLine(sfd.ToString() + "\r\n");
  }
}

The StackFrameData Class

Here, I use a helper class, StackFrameData, to extract what I want to report from the StackFrame:

public class StackFrameData
{
  public string FileName { get; private set; }
  public string Method { get; private set; }
  public int LineNumber { get; private set; }

  public StackFrameData(StackFrame sf)
  {
    FileName = sf.GetFileName();
    Method = sf.GetMethod().Name;
    LineNumber = sf.GetFileLineNumber();
  }

  public override string ToString()
  {
    return $"{FileName}\r\n{Method}\r\n{LineNumber}";
  }
}

Now, we're getting somewhere:

Image 2

The ExceptionReport Class

With another helper, the exception is easily serializable into JSON for something that becomes machine readable. Given:

catch (Exception ex)
{
  var report = new ExceptionReport(ex);
  string json = JsonConvert.SerializeObject(report);
  Console.WriteLine(json);
}

and the ExceptionReport helper class:

public static class ExceptionReportExtensionMethods
{
  public static ExceptionReport CreateReport(this Exception ex)
  {
    return new ExceptionReport(ex);
  }
}

public class ExceptionReport
{
  public DateTime When { get; } = DateTime.Now;  

  [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
  public string ApplicationMessage { get; set; }

  public string ExceptionMessage { get; set; }

  public List<StackFrameData> CallStack { get; set; } = new List<StackFrameData>();

  [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
  public ExceptionReport InnerException { get; set; }

  public ExceptionReport(Exception ex)
  {
    ExceptionMessage = ex.Message;
    var st = new StackTrace(ex, true);
    var frames = st.GetFrames() ?? new StackFrame[0];
    CallStack.AddRange(frames.Select(frame => new StackFrameData(frame)));
    InnerException = ex.InnerException?.CreateReport();
  }
}

and we have a nice JSON object:

Image 3

Notice we're also finally reporting the inner exception!

A Catcher in the Rye

While there will probably be some kicking and screaming here, I'm going to take this one step further. I don't always want to be writing:

try
{
  Stuff();
}
catch(Exception ex)
{
  var report = new ExceptionReport(ex);
  string json = JsonConvert.SerializeObject(report);
  // Log somewhere
}

and I don't want to rely on other developers doing this right either. Sure, I could do this:

try
{
  Stuff();
}
catch(Exception ex)
{
  Logger.Log(ex);
}

but I still have this "pollution" of the try-catch statements. What about (here's the kicking and screaming part) writing this instead:

Catcher.Try(() => SayHi("Hi"));

with the help of:

public static class Catcher
{
  public static T Try<T>(Func<T> fnc, Action final = null)
  {
    try
    {
      T ret = fnc();
      return ret;
    }
    catch (Exception ex)
    {
      Log(ex);
      throw;
    }
    finally
    {
      final?.Invoke();
    }
  }

  public static void Try(Action fnc, Action final = null)
  {
    try
    {
      fnc();
    }
      catch (Exception ex)
    {
      Log(ex);
      throw;
    }
    finally
    {
      final?.Invoke();
    }
  }

  private static void Log(Exception ex)
  {
    var report = new ExceptionReport(ex);
    string json = JsonConvert.SerializeObject(report, Formatting.Indented);
    Console.WriteLine(json);
  }
}

Now we get:

Image 4

There are two problems with this:

  1. We're getting the stack trace including the Try call (the now anonymous method in main I'm going to let slide. There's always side-effects!)
  2. The Catcher re-throws the exception, which may or may not be what we really want.

The first problem is solved like this, where on the exception in the Catcher class, we tell the reporter to skip the last stack item:

Log(ex, 1);

The reporter is then called like this:

private static void Log(Exception ex, int exceptLastN)
{
  var report = new ExceptionReport(ex, exceptLastN);
  string json = JsonConvert.SerializeObject(report, Formatting.Indented);
  Console.WriteLine(json);
}

With:

public static class ExceptionReportExtensionMethods
{
  ...
  public static T[] Drop<T>(this T[] items, int n)
  {
    return items.Take(items.Length - 1).ToArray();
  }
}

and in the reporter:

var frames = st.GetFrames()?.Drop(exceptLastN) ?? new StackFrame[0];

Now we see:

Image 5

As to the second issue, we can implement a SilentTry static method that doesn't re-throw the exception, for example:

public static void Try(Action fnc, Action final = null)
{
  try
  {
    fnc();
  }
  catch (Exception ex)
  {
    Log(ex, 1);
  }
  finally
  {
    final?.Invoke();
  }
}

Expanding the test jig:

static void Main(string[] args)
{
  Catcher.SilentTry(() => SayHi("Hi"), () => Console.WriteLine("Bye"));
  var ret = Catcher.SilentTry(() => Divide(0, 1), () => Console.WriteLine("NAN!"));
  Console.WriteLine(ret);
}

static void SayHi(string msg)
{
  throw new Exception("oops", new Exception("My Inner Self-Exception"));
}

static decimal Divide(decimal a, decimal b)
{
  return a / b;
}

(Did you know that only decimal and integer types throws a divide by zero exception?  Double returns "infinity"!)

So now, we get two log entries and finally execution:

Image 6

And notice, because these are silent tries, no exception is thrown in the caller. Hmmm....

The Kitchen Sink

Or the garbage disposal (more screaming I hear!). Because the Func<T>; call is the most complicated:

public static bool SilentTry<T>(Func<T> fnc, out T ret, 
              Action final = null, T defaultValue = default(T), Action onException = null)
{
  bool ok = false;
  ret = defaultValue;

  try
  {
    ret = fnc();
    ok = true;
  }
  catch (Exception ex)
  {
    Log(ex, 1);
    onException?.Invoke();
  }
  finally
  {
    final?.Invoke();
  }

  return ok;
}

And our test jig:

bool ok = (Catcher.SilentTry(
  () => Divide(5, 2),
  out decimal ret,
  defaultValue: decimal.MaxValue,
  onException: () => Console.WriteLine("Divide by zero!!!"),
  final: () => Console.WriteLine("The answer is not 42.")
));

Console.WriteLine($"Success? {ok} Result = {ret}");

A non-exception:

Image 7

And on a divide by zero by calling () => Divide(5, 0),

Image 8

OK, that's pretty insane but it illustrates what you can do with optional parameters.

Questions

It gets more insane:

  • What happens when an exception occurs in the finally block or the onException call? 
  • What happens when the logger throws an exception?!?!?!
  • What about catching a specific exception in this manner?

The first question is "easily" handled by using the Catcher with the help of a couple extension methods:

public static void Try(this Action action)
{
  Catcher.Try(action);
}

public static void SilentTry(this Action action)
{
  Catcher.SilentTry(action);
}

And implementing the finally as a conditional Try or SilentTry on the final action:

public static bool SilentTry<T>(Func<T> fnc, out T ret, 
              Action final = null, T defaultValue = default(T), Action onException = null)
{
  bool ok = false;
  ret = defaultValue;

  try
  {
    ret = fnc();
    ok = true;
  }
  catch (Exception ex)
  {
    Log(ex, 1);
    SilentTry(() => onException?.Invoke());
  }
  finally
  {
    final?.SilentTry();
  }

  return ok;
}

The second question is solved by having a try-catch around the logging that can attempt then to do something else:

private static void Log(Exception ex, int exceptLastN)
{
  try
  {
    var report = new ExceptionReport(ex, exceptLastN);
    string json = JsonConvert.SerializeObject(report, Formatting.Indented);
    Console.WriteLine(json);
  }
  catch (Exception loggerException)
  {
    // Log failure handler
  }
}

The third question can be solved by using a generic parameter for the type you specifically want to catch. Again, using the Func SilentTry implementation:

public static bool SilentTry<E, T>(Func<T> fnc, out T ret, Action final = null, 
       T defaultValue = default(T), Action onException = null) where E: Exception
{
  bool ok = false;
  ret = defaultValue;

  try
  {
    ret = fnc();
    ok = true;
  }
  catch (Exception ex)
  {
    Log(ex, 1);
    onException?.Invoke();

    if (ex.GetType().Name != typeof(E).Name)
    {
      throw;
    }
  }
  finally
  {
    final?.SilentTry();
  }

  return ok;
}

Now notice with (note the explicit parameter usage for readability):

bool ok = (Catcher.SilentTry<Exception, decimal>(
  () => Divide(5, 0),
  out decimal ret,
  defaultValue: decimal.MaxValue,
  onException: () => Console.WriteLine("Divide by zero!!!"),
  final: () => Console.WriteLine("The answer is not 42.")
));

the exception is thrown to the caller:

Image 9

Whereas, if we get the exception we want:

bool ok = (Catcher.SilentTry<DivideByZeroException, decimal>(
  () => Divide(5, 0),
  out decimal ret,
  defaultValue: decimal.MaxValue,
  onException: () => Console.WriteLine("Divide by zero!!!"),
  final: () => Console.WriteLine("The answer is not 42.")
));

it's not:

Image 10

The only issue with the above code is now we have to specify explicitly the exception type we handle silently and the return parameter type. Or you might like version:

public static bool SilentTry<E>(Action action, Action final = null, 
Func<bool> onOtherException = null, Func<bool> onSpecifiedException = null) where E : Exception
{
  bool ok = false;

  try
  {
    action();
    ok = true;
  }
  catch (Exception ex)
  {
    Log(ex, 1);

    if (ex.GetType().Name == typeof(E).Name)
    {
      SilentTry(() => ok = onSpecifiedException?.Invoke() ?? false);
    }
    else
    {
      SilentTry(() => ok = onOtherException?.Invoke() ?? false);
    }
  }
  finally
  {
    final?.SilentTry();
  }

  return ok;
}

So this allows you to do something like this:

if (!Catcher.SilentTry<SqlException>(() =>
  () => GetDataFromDatabase(),
  onSpecifiedException: () => GetDataFromLocalCache()))
{
  HandleTotalFailureToGetData();
}

Crazy, right?  I suspect most people prefer the equivalent:

try
{
  GetDataFromDatabase();
}
catch(SqlException)
{
  try
  {
    GetDataFromLocalCache();
  }
  catch
  {
    HandleTotalFailureToGetData();
  }
}
catch
{
  HandleTotalFailureToGetData();
}

Yuck. I don't, mainly because the intention of the code is a lot harder to read with the catch blocks.

Throw vs. Throw ex

A quick note here. There is almost never a reason to use throw ex; as this resets the exception stack:

Given:

Catcher.Try(ThrowSomething);

...

static void ThrowSomething()
{
  throw new Exception("Foobar");
}

throw gives you:

Image 11

Notice here that the exception stack shows the call to ThrowSomething.

If I change the catcher to use throw ex; we get:

Image 12

Notice that the exception stack is reset and that we're now seeing the stack starting from the Catcher.Try method. We've lost the method ThrowSomething that threw the exception!

Inner Exceptions

Inner exceptions are useful when you're throwing an exception as a result of handling another exception. Using the above example where we have a fallback for getting data from a local cache, we might write this:

try
{
  GetDataFromDatabase();
}
catch(SqlException ex)
{
  if (!cacheExists)
  {
    throw new Exception("Cache doesn't exist", ex);
  }
}

Here, the inner exception tells us that the an exception was initially raised getting the data from the base, and because we don't have a local cache, we're throwing an exception because the fallback failed as well. We could implement the Try method a bit differently, allowing the onException Action to specify a fallback:

public static void Try(Action fnc, Action final = null, Action onException = null)
{
  try
  {
    fnc();
  }
  catch (Exception ex)
  {
   Log(ex, 1);
   try
    {
      onException?.Invoke();
    }
    catch (Exception ex2)
    {
      Log(ex2, 1);
      var newException = new Exception(ex2.Message, ex);
      Log(newException, 1);
      throw newException;
    }
    finally
    {
      final?.Try();
    }
  }
}

It seems the finally block only makes sense if the exception fallback fails. If we try this out:

Catcher.Try(GetDataFromDatabase, onException: GetDataFromCache);

With the fallback failing as well:

static void GetDataFromDatabase()
{
  throw new Exception("EX: GetDataFromDatabase");
}

static void GetDataFromCache()
{
  throw new Exception("EX: GetDataFromCache");
}

We see the log with the inner exception:

Image 13

Notice though that we don't get a call stack nor filename and method for the outer exception because we've created a new Exception object, passing in only the fallback's exception message. This is really annoying and is in my opinion a shortcoming of the Exception class. As the screenshot shows, we did get a correlating log of both inner and outer exceptions, which can be consolidated. First, the Try method:

public static void Try(Action fnc, Action final = null, Action onException = null)
{
  try
  {
    fnc();
  }
  catch (Exception ex)
  {
    try
    {
      onException?.Invoke();
    }
    catch (Exception ex2)
    {
      Log(ex2, ex, 1);
      var newException = new Exception(ex2.Message, ex);
      throw newException;
    }
    finally
    {
      final?.Try();
    }
  }
}

Then a Log method that combines the two exceptions:

private static void Log(Exception outer, Exception inner, int exceptLastN)
{
  try
  {
    var outerReport = new ExceptionReport(outer, exceptLastN);
    var innerReport = new ExceptionReport(inner, exceptLastN);
    outerReport.InnerException = innerReport;
    string json = JsonConvert.SerializeObject(outerReport, Formatting.Indented);
    Console.WriteLine(json);
  }
  catch (Exception loggerException)
  {
    // Log failure handler
  }
}

Notice that the log is only created when the exception fallback fails:

Image 14

That seems more useful and is a nice demonstration of how wrapping try-catch with some smarts can actually improve the results you get from a log.

Debug vs. Release

When you change the build to "Release", Visual Studio still generates a PDB file:

Image 15

This is the file that's used to provide the line number and source filename in the Exception class and therefore which is used when generating the stack trace via the StackTrace class. If you don't want the PDB file in the release, you have to change the Advanced Build Settings. First, set your project for release mode from the Build -> Configuration Manager dialog:

Image 16

Then right-click on the project and select the Build section, then click on "Advanced" and select "None" for the debug information output:

Image 17

Unfortunately, using the IDE, this has to be done for each project in the solution. It is simpler to just omit the PDB files in whatever process you use to move the application to the production environment, or, if you're using msbuild on the command line, add this to the release settings configuration file:

<DebugSymbols>false</DebugSymbols>
<DebugType>None</DebugType>

or, from the command line, specify:

/p:DebugSymbols=false /p:DebugType=None

Regardless of how you do it, you'll now notice that the exception is in its raw form, and therefore the formatted JSON that the code here produces, no longer has the filename or line number:

Image 18

We can remove the null and 0 with a little refactoring of the StackFrameDate class:

public class StackFrameData
{
  [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
  public string FileName { get; private set; }

  public string Method { get; private set; }

  [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
  public int? LineNumber { get; private set; }

  public StackFrameData(StackFrame sf)
  {
    FileName = sf.GetFileName();
    Method = sf.GetMethod().Name;
    int ln = sf.GetFileLineNumber();
    LineNumber = ln == 0 ? new int?() : ln;
  }

  public override string ToString()
  {
    return $"{FileName}\r\n{Method}\r\n{LineNumber}";
  }
}

Yielding:

Image 19

Conclusion

This has been a wild and wooly ride (just the thing I like) into basically syntactical sugar coating (though you may not like it) of syntax for handling exceptions. While the point of this article was initially more focused on creating a machine-readable log report, I hope the seed has been planted for creating a consistent exception handling approach. You may not like the syntactical sugar coating, but hopefully you'll get some ideas from this article for a solution that works for you.

History

  • 15th July, 2019: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Marc Clifton
Architect Interacx
United States United States
Blog: https://marcclifton.wordpress.com/
Home Page: http://www.marcclifton.com
Research: http://www.higherorderprogramming.com/
GitHub: https://github.com/cliftonm

All my life I have been passionate about architecture / software design, as this is the cornerstone to a maintainable and extensible application. As such, I have enjoyed exploring some crazy ideas and discovering that they are not so crazy after all. I also love writing about my ideas and seeing the community response. As a consultant, I've enjoyed working in a wide range of industries such as aerospace, boatyard management, remote sensing, emergency services / data management, and casino operations. I've done a variety of pro-bono work non-profit organizations related to nature conservancy, drug recovery and women's health.

Comments and Discussions

 
QuestionCSharp.OperationResult Pin
kiquenet.com25-Jul-19 6:04
professionalkiquenet.com25-Jul-19 6:04 
QuestionC# Functional Pin
kiquenet.com25-Jul-19 6:00
professionalkiquenet.com25-Jul-19 6:00 
QuestionTuples (Error, Result) Pin
kiquenet.com25-Jul-19 5:48
professionalkiquenet.com25-Jul-19 5:48 
QuestionInteresting Pin
carloscs19-Jul-19 4:40
membercarloscs19-Jul-19 4:40 
QuestionThe Kitchen Sink Pin
Glen Harvy16-Jul-19 13:43
memberGlen Harvy16-Jul-19 13:43 
AnswerRe: The Kitchen Sink Pin
Marc Clifton17-Jul-19 6:26
protectorMarc Clifton17-Jul-19 6:26 
QuestionUnable to find two uncommented Actions Pin
Member 257594416-Jul-19 12:18
memberMember 257594416-Jul-19 12:18 
AnswerRe: Unable to find two uncommented Actions Pin
Marc Clifton17-Jul-19 6:28
protectorMarc Clifton17-Jul-19 6:28 
GeneralMy vote of 5 Pin
MarkNB15-Jul-19 12:30
memberMarkNB15-Jul-19 12:30 
GeneralIs StackTrace Class trustworthy? Pin
Tammam Koujan14-Jul-19 21:48
professionalTammam Koujan14-Jul-19 21:48 
GeneralRe: Is StackTrace Class trustworthy? Pin
Marc Clifton15-Jul-19 3:06
protectorMarc Clifton15-Jul-19 3:06 
GeneralRe: Is StackTrace Class trustworthy? Pin
Tammam Koujan17-Jul-19 22:58
professionalTammam Koujan17-Jul-19 22:58 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Article
Posted 14 Jul 2019

Tagged as

Stats

4.7K views
82 downloads
34 bookmarked