DEV Community

loading...

Exception handling explained

Jonas Gauffin
Founder of Coderr - Automated error handling
・7 min read

This article explains what exception handling is and how it differs from
traditional error handling with error codes. The article does not get
into usage or exception classes, but only to explain their purpose.

Let's start with errors.

A brief introduction to error

From the dawn of programming, error codes have been used to deal with errors in applications. An error code is used to indicate if the
executed function was successful or not.

Here is a simple example:

public bool SaveDefaultAccount(string userName, string accountName)
{
    int userId = GetUserIdFromName(userName);
    if (userId == -1)
        return false;

    int accountId = GetAccountFromName(accountName);
    if (accountId == -1)
        return false;

    return StoreDefaultAccount(userId, accountId);
}

The example illustrates that when you use error codes, it is the API
consumer that must abort if something fails. You might, but how about
the rest of the team, or those who maintained the application before
you? It is like you would retire the entire police force and expect
all citizens to behave and be good law abiding citizens.

One developer could have been lazy and just written (or refactored) the
code as:

public bool SaveDefaultAccount(string userName, string accountName)
{
    int userId = GetUserIdFromName("Arne");
    int accountId = GetAccountFromName("Savings account");
    return StoreDefaultAccount(userId, accountId);
}

The problem is that the code looks perfectly legal, but silently ignores
errors. If you for instance mistakenly add a white space after all
account names on the account selection page, the code above starts to
fail silently. What's worse is that you will not know about it until
code dependent upon the default account starts to fail, and that can be
after a while. Tracking down that subsequent error can be challenging,
primarily if the default account is set in different ways.

Error codes are easy to get started with and can be quite powerful. Even
modern languages like Go-lang uses errors instead of exceptions. Here is
a go snippet:

f, err := os.Open("filename.ext")
if err != nil {
    log.Fatal(err)
}

While errors are easy to understand and use, they have three
significant drawbacks:

They do not convey context

This section is for languages that only uses error codes.

For instance, error code 2 means "File Not Found" in Windows. There is
no way to state which file nor other information that might help you to
understand why the file was missing. Error codes are just that. Codes
that indicate a specific error, without context or clues.

The problem with that is that it is hard to understand why or under what
circumstances that the error occurred. The Windows API solves this by
introducing a method called GetLastError() which is used to get more
information about the error.

OFSTRUCT buffer;
HFILE hFile = OpenFile("d:\\sample.txt", &buffer, OF_READ);
if (hFile == HFILE_ERROR)
{
    // all this is required to get the error message.
    // you typically add it to a separate function

    // Get the error code, as hFile only indicates an error
    // but not which one.
    var errorCode = GetLastError();

    // Get the generic error message which
    // represents the above error code.
    LPVOID lpMsgBuf;
    LPVOID lpDisplayBuf;
    FormatMessage(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | 
        FORMAT_MESSAGE_FROM_SYSTEM |
        FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL,
        errorCode,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPTSTR) &lpMsgBuf,
        0, NULL );
}

My point is that an error code is not enough when you want to
solve the error.

The burden is on the function caller

It is safe to say that all applications have errors, it is exceptionally
rare (pun intended) that errors can be ignored. Ignoring errors might
seem to work, but sooner or later consequential problems will surface.
It will be much harder to find the root cause since the found error is
just a consequence of the first one. Any kind of "fix" is just a
workaround which clutters the code base but does not prevent the root
cause from happening again.

It is crucial that all errors are handled in your code. When you use
error codes, it is so easy to ignore or forget errors. Had a tight
deadline? Wrestled with an obscure bug or incomprehensible requirements?
Those situations make it so easy to take a shortcut. If not all
developers on your team have the same discipline, errors will get
ignored.

Enter exceptions

Exceptions are for exceptional situations. If something didn't go as
expected, you got an exceptional situation.

Sounds easy, huh?

But what does that mean?

An exceptional example

If you expect that something can fail, you should guard against that
situation.

Here is an example:

Let's say that you love shopping the newest and hottest technical
gadgets. When the new gadget is released, you want to be first.

Scenario 1

If you are like most of us, you can probably not just go on a shopping
spree. You need to make sure that you have enough money.

  1. Check your bank account.
  2. Go shopping.
  3. Pay

Scenario 2

However, if you are fortunate enough to have plenty of money, you can
go shopping directly:

  1. Go shopping.
  2. Pay

The difference

In the first scenario, we have a known issue that we need to deal with:
A money limit. Therefore, we always need to check that we have enough
money. In the second scenario, we should have enough money.

What happens if someone has hacked us in scenario 2:

  1. Someone hacked us
  2. Go shopping
  3. Pay <-- Failed, no money

In that case, we got an exception, since it is a case that shouldn't
happen since we expect to have enough money.

That is an exceptional situation.

Why can't we always write the code like in scenario 1?

First, it's about communicating intent. We mostly get a set of business
requirements that we should fulfill. They tell us what to expect when
implementing different use cases. If we go and add many checks for
things that might, but should not, happen we lose the connection between
our use cases and the code. It will be hard to tell the intent of the
code, which in turn lead to assumptions and in the end decreased code
quality.

Second, if we add many checks we are coding workarounds as the real
problem is that our account was hacked, not that we could not pay. By
adding checks and abort instead of paying we are hiding that fact.

What exceptions are

Exceptions are for situations that wasn't considered when defining what
the application should do. When writing a messaging library for message
queues you expect to receive complete messages, but when you write one
for TCP you expect to receive partial messages. What's exceptional in
one case doesn't necessarily have to be exceptional in another.

If you would open a file there are several errors that can happen.
Non-existent directory, file is missing, access denied, partial file,
etc. All those errors are known to most programmers, so they are not
exceptions, right?

Wrong. In most cases, they are exceptions. Because you typically do
expect that a file exists and that it's complete and readable. Well, if
you are writing a data forensics application, most of those errors are
expected and should be dealt with (and therefore not exceptional cases).

Exceptions exist to prevent your application from doing something stupid.

It's crucial for you to understand that. Don't think of exceptions as
something you can use to control your application when coding. Think of
exceptions as a mechanism to guard against your application doing
something wrong/unexpected.

Exceptions exist to help you fix problems in the future

Since exceptions are not a flow control mechanism, they do add little
value when executing your code (previously described point excluded).

However, writing informative exception messages makes it much easier to
correct future bugs, since they add context to the error. Always try to
do so, your future self will thank you for it.

Code example

Let's take the same code as was found in the beginning of this article,
but changed to use exception handling instead.

public void SaveDefaultAccount(string userName, string accountName)
{
    int userId = GetUserIdFromName("Arne");
    int accountId = GetAccountFromName("Savings account");
    StoreDefaultAccount(userId, accountId);
}

As you can see, the method now returns void as it does not need to
indicate that everything went successfully. Nor does it need to validate
error codes from the called methods. One can safely assume that an
exception abort the processing if the expected result cannot be
guaranteed.

In fact, in most cases, we do not have to care if exceptions are thrown
at all. Remember, exceptions are used to communicate that something
unexpected happened. If we cannot predict it, how on earth could we be
able to handle the exception?

Let's look at the StoreDefaultAccount method. The most important thing
to understand is that the method says that a default account should be
stored successfully. Since that is the method promise, we must throw an
exception every time we find something that would prevent the default
account from being stored.

public void StoreDefaultAccount(int userId, int accountId)
{
    if (userId <= 0)
        throw new ArgumentOutOfRangeException(nameof(userId), userId,
            "A valid user id must be specified.");
    if (accountId <= 0)
        throw new ArgumentOutOfRangeException(nameof(accountId), accountId,
            "A valid account id must be specified.");

    var account = _accountRepository.GetById(accountId);
    if (account.OwnerId != accountId)
        throw new InvalidOperationException($"User {userId} do not own account {accountId}.");

    var user = _userRepository.GetById(userId);
    user.DefaultAccount = accountId;
    _userRepository.Update(user);
}

The first two exceptions are used to mitigate errors like parse errors
or invalid data.

The third exception is for a business rule. We may only use the user's
own accounts as default accounts.

_accountRepository.GetById(accountId); will in turn throw an
exception if the given accountId do not exist in the database since the
method name states that an account should be fetched.

Summary

The purpose of exceptions is not to allow you to take different actions
depending on if something failed or not. i.e.Β they are not a flow
control mechanism. Instead, exceptions are used to make sure that your
application delivers the expected result (or die trying).

With that in mind, I hope that you find them as useful as I do. With
the right mindset (and using exceptions) you can save much time since
you do not have to track down why your database has a lot of data
inconsistencies (which leads to bugs later).

This article is part of our exception handling series. To learn more, visit our website.

Discussion (3)

Collapse
adriannull profile image
adriannull

I'm sorry but i find this way of using exceptions extremely confusing and i find that it really breaks the flow of thinking about the code.

Error codes can be made to be verry clear about the problem they are signaling, if you define one value for every possible problem you could encounter. So then you won't be confused about what happened and you will know what to do to fix it.

Exceptions, on the other hand, i find it normal to be thrown only by the OS (or runtime) when something serious happens and your application would normally crash. Like division by zero. But not when a folder is not found, file cannot be opened and situations like these, which are normal to happen so are part of the usual workflow of your program.

I stumbled across this problem when i had to code in C# a while ago. I was used to C/C++ way of thinking and coding and i had quite a surprise when my application crashed because it needed to list files from a folder that did not existed. I simply expected it not to give me any results, but not crash because it threw an exception that i did not catch. I find that if you're forced to wrap every piece of code that can break in a try {} catch() block, your entire flow of thinking breaks and your code actually becomes harder to read and follow. It's also sad that there are languages, like C#, that force this way to you :(

Collapse
jgauffin profile image
Jonas Gauffin Author • Edited

I was used to C/C++ way of thinking and coding and i had quite a surprise when my application crashed because it needed to list files from a folder that did not existed

If the folder should exist (created/handled by the code) it is an exception if it do not exist. Because the rest of that code can not function properly without it.

If it is expected that the directory may not exist, it's actually a bug if you do not use Directory.Exists before using the folder. If you only had got an error code it would have been much harder to understand what went wrong. (If you do not check if the directory exists you would typically not have checked for an "directory not found" error code).

I find that if you're forced to wrap every piece of code that can break in a try {} catch() block, your entire flow of thinking breaks and your code actually becomes harder to read and follow.

You should not wrap every part of the code with try/catch. That's the point. They are there to safeguard that your application don't save invalid results when something goes wrong.

If you can't handle the exception, you should not catch it. Most programming languages/frameworks that use exceptions have global hooks where you can catch them instead of polluting your entire application with try/catch blocks.

Collapse
mbzmak profile image
MAK

Thx for sharing.
Jonas, I think that the StoreDefaultAccount method, the way you wrote it, will always fail with an InvalidOperationException since every account's owner ID never equals the account ID.
The test should be :

if (account.OwnerId != userId)

Forem Open with the Forem app