Wednesday 23 November 2022

Functional Library - Errors

The success path for any given application is an easy path follow. Problems arise when errors need to be dealt with. I am of the mindset that exceptions are significantly different to errors. An exception, is typically a showstopper, null pointer exception, stack-overflow exception, out of memory exceptions. These type of exceptions usually result in a program crash and graceful handling can be difficult.

The other end of the scale consists of errors that one can typically handle without detrimental effect to the application. Examples include incorrect input, failure to connect to a database, a socket etc. The latter examples might be down to network failures so retries may be possible. In contrast one cannot perform a retry if an exception is raised (assuming the above exceptions).

Like I say this is not easy! There are other issues. Exceptions create a stack trace, extremely useful for debugging. Manual error handling offers no such gifts and can make debugging much harder.

In this post...

Exceptions vs Manual Error Handling

To avoid complexity, let's keep things simple. Exceptions denote exceptional events, null pointers, stack overflows, out of memory, etc. So, essentially, system errors. Typically, exceptions of the aforementioned results in program failure.

In contrast errors denote problems that do NOT necessarily result in program failure. Examples and possible solutions follow...

  • Entering an invalid email address. In this scenario, one could just prompt user to enter a new (valid) email address.
  • Database connection failure. The database might be offline, network maybe down. One could save transactional data to a file to be run later. This is a tricky problem and depends upon the database transaction that was about to occur. Context is the key, one needs to view the impact on the application.
  • Socket connection failure. Same techniques for database failure might be applied. Retry strategies in both scenarios will be required.
Top

Functions

Functions generate results or perform an action. Typically, in OO solutions, actions do not yield a result and may, instead throw an exception upon failure. Whilst throwing an exception might be the way forward, function signatures typically do not state that an exception may be thrown. Typically, one must read supporting documentation to reveal if a function may throw an exception. Throughout my career I have encountered code that calls a function and ignores any exceptions that might be thrown. This especially tends to be the case if said function can throw any number of exceptions.

I do not think exceptions are a bad thing, however, I do believe they are misused and a better mechanism is required.

Top

Function Honesty

Consider the following function...

int Divide(int number, int divisor)
{
  return number / divisor;
}

Clearly, if the divisor is zero a divide by zero exception will be thrown. Of course the function signature does not convey this. How can we deal with this type of scenario? I believe there are three ways...

  • Return a sentinel value
    In this case NaN.
  • Throw a divide by zero exception.
  • Return a result which either contains a valid value or a reason as to why the function failed.

Analysing the above I would come to the following conclusions...

  • The NaN informs me that the function resulted in a non-number, doesn't tell me why, but, at least I know the function failed. In this simple example one could probably fathom that the function failed as the divisor was zero. Functions exist in numerous libraries that use sentinel values where usage may not be so obvious.
  • The exception informs me why the function failed. However, I might not be in the correct place to catch and respond to the exception. Also, the function does not state that it might throw an exception. So, why would I be inclined to respond to exceptions.
  • The result solution, in this case, might be best. I can call the function and act accordingly upon the result. If the result denotes success, on my merry way I go. If the result denotes failure, I might retry, log the error and fail gracefully, maybe try a different execution path. Best of all, the function signatures indicates that the function may succeed or fail.

Now, consider the following function declaration...

int GetChar();

Most will understand that char is short for character. As such, one might assume that the function returns a character. However, this is clearly not the case as the function returns an int (signed integer). The reason for this is that -1 is a sentinel value that indicates end of file. Of course you will only know this if you read the documentation and said documentation is both accurate and up to date. Personally, I think a better way to to handle this situation is to use some kind of iterator. For example...

class ReadCharacters{
  public bool IsEof {get; private set;}
  public char Current { get; private set; }
  
  public bool Next()
  {
    int ch = GetChar();
    if (ch == -1)
    {
      IsEof = true;
      return false;
    }
    Current = (char)ch;
    return true;
  }
}

I am not going to pretend that the above code is foolproof. However, I think it conveys more information than "GetChar" and is harder to misuse. Both are good properties that help produce code with fewer bugs.

Top

No comments:

Post a Comment