Error Handling - The Basics
Understanding errors and error handling at a basic level is valuable to build a strong programming foundation. In this post I will outline some high level concepts that are useful to know to get started:
- Are errors bad?
- Errors versus exceptions
- Unchecked versus checked exceptions
- The call stack
- The stack trace
- Catching and throwing
- Propagation
Are errors bad? #
To start, it's important to know that errors are not bad. At least not from a programmer perspective.
Errors returned in software are intentional communication tools: they let a user know that their program has reached a situation that it cannot handle. It wants the user to know what happened and why. Armed with this information, the user can decide what to do next.
All programming languages I have ever used include error management libraries that allow developers to easily respond to and communicate these situations. If a user is seeing an error message, that is a good thing. Situations where the program becomes unresponsive or crashes without warning are bad.
If you're just beginning coding, you might think that well built software never fails. In reality, software often has external dependencies and failure is not always in our control. Other times it is critically important for the software to fail rather than continuing, such as when faced with bad user input.
Quality software:
- fails gracefully when it cannot continue
- uses errors intentionally to handle failure scenarios
- provides good information through thoughtful error messages
Errors versus exceptions #
I'll be honest: in 99% of my conversations with other developers, these terms are used interchangeably. You will be understood regardless of which term you use, and the conversation will not stall out in any reasonable setting.
It's good to understand that there is a difference (and avoid technical pedantry).
The generally agreed upon distinction is:
- Exceptions are recoverable, and are meant to be handled
- Errors are unrecoverable, and cannot reasonably be handled
What does unrecoverable mean? Generally, it means that the program cannot continue and must exit.
A common example would be a compiler error. When a compiler hits code it cannot handle, it throws an error rather than trying to compile the rest of the program.
Some examples: #
-
javascript might throw a
TypeError
orReferenceError
to let you know that your code itself cannot be parsed in it's current state: the javascript engine can't understand it. Inside your code, you can handle Exceptions like aDomException
. Confusingly, there is noException
in javascript natively, theError
module is used for this purpose. -
PHP allows you to end a program with
die(...)
which is an explicit unrecoverable error. You can alsothrow new Exception(...)
inside your code for situations where another part of your code might be able to handle it. -
java has more explicit code standards where errors indicate system-level issue that should not be caught. Exceptions are broken down further and can either be checked and unchecked.
Unchecked versus checked exceptions #
In conversation or online you might hear the terms checked exception or unchecked exception. The checked aspect refers to being type-checked at compile time:
-
Checked exceptions are enforced through typing. A method that throws an exception forces it's caller (outer program) to handle it. If used, all possible exceptions returned from a method must be declared ahead of time so that they can be validated by the compiler.
-
Unchecked exceptions can be handled, but it is not enforced at compile time. Allowing them to be created and not handled directly means that there is a possibility they aren't handled at all. That situation is what checked exceptions aim to avoid.
Either can be used, and the choice comes down to code standards and preference. Some believe that checked exceptions ensure proper error handling. Others believe it creates unnecessary coupling and overhead.
Most languages only have unchecked exceptions, which can be thrown from anywhere in the application during runtime and there is no guarantee that they will be handled. This is either considered a huge feature or big point of failure depending on who you talk to.
The call stack #
In any application of reasonable size, there are a lot of pieces of code interacting with each other. You will have a chain of code functions calling functions calling functions. It's important to understand this in order to understand errors and error handling.
Keeping track of what the current step is, and what code called it, is the call stack. Each function that is 'call'ed is added to the call stack. If it then calls another function, that one is added after it. When a function completes, it is removed from the call stack and the result is passed back to the previous function.
Pushing to the callstack #
A practical example would be fetching a website's homepage via a browser. I've added mocked up simple function examples just to help things along.
First we have to trigger all of the code to get our expected response data, the HTML of the page:
- browser triggers
handleRequest
: through an http request, the website server receives a request and begins processing it. handleRequest
callsresolveRoute
: the website passes the url to it's routerresolveRoute
callsbuildHomepage
: the router finds and triggers page level code to handle the requestbuildHomepage
calls getHomepagePosts`: that resolver calls a data service to get the appropriate informationgetHomepagePosts
callsMySQLClient
: the data service makes a request to the database
If we were to look at the call stack at this point, we might see something that indicates the following:
[handleRequest, resolveRoute, buildHomepage, getHomepagePosts, MySQLClient]
Removing from the callstack #
Each function is waiting for the one after it before it can complete it's work. Now that we have the data, we work backwards to complete the browser's request:
MySQLClient
returns a post list data togetHomepagePosts
getHomepagePosts
returns the same data tobuildHomepage
buildHomepage
generates HTML based on the data toresolveRoute
resolveRoute
passes the HTML tohandleRequest
handleRequest
streams the HTML back to the browser- the browser displays the page
There are realistically many more steps in between the ones described, but even with this simplified flow we have a going on. Each layer is waiting on the one under it to succeed so that it can complete it's work. At each point, an error could occur.
The Stack Trace #
Every function called along the call stack has the potential for error. The details of what went wrong should be provided by your Exception
. This should include a good error message, but sometimes more detailed information is helpful to debug what went wrong.
One of the most common pieces of information provided by an Exception is the stack trace. It takes a snapshot of the callstack at the point the Exception occurred and stores it so it can be read later.
Managing stack traces #
Most built-in error modules, like Javascript's Error
, have you covered and automatically add this information for you. Just be sure that you don't accidentally break this functionality by improperly extending the built in modules for your use cases.
It's also important to note that stack traces can offer a lot of information about your system, and it's unlikely you want the average end user to have access to it. In production, this information is removed from any final error responses that are shown to your user, but still sent via logs to the development team to help with debugging.
Why is the stacktrace so useful? It's due to the nature of unchecked exceptions. Sometimes, the code that triggered a function might not attempt to handle it.
In fact, it is often good practice to wait as long as possible before handling an error, and that code that does so might much higher up the call stack than where it originated. This is the concept behind and the origin of the terms throwing and catching.
Catching and throwing #
Languages like Javascript have built in features that allow you to throw
an Exception that occurs at any point in your code. I assume it was imagined like a ball being tossed up in the air. In our case if it isn't caught it will cause our program to crash.
Before we start mixing metaphors, take a look at the following Javascript example:
var name = null;
function getName() {
throw new Error("Name not found");
}
function getUserDetails() {
const name = getName();
return name;
}
function userInformation() {
try {
return getUserDetails();
} catch(e) {
console.log(e.stacktrace);
return 'Unable to get user details';
}
}
// then 'Unable to get user details'
console.log(userInformation());
As you can see above, we have a call stack of:
[getUserDetails, userInformation, getName]
in getName
you can see the built in throw
being used on a new Error
. The userInformation
function implements a try/catch block to catch
it.
How does this work? Through propagation.
Propagation #
In the above example you can see that an exception occurred during getName
function. The call stack looks like:
- userInformation() -> Exception caught here
- getUserDetails() -> no catch
- getName() -> Exception thrown here
Each function in the call stack has the opportunity to catch
as well as throw
, but they also can do nothing. The exception moves up in the call stack until it either is caught in a try/catch block or hits the end of the program and (likely) causes it to crash.
The Exception moving higher up the call stack before being handled is sometimes also referred to 'bubbling up' and 'climbing'.
Further Reading #
- https://developer.mozilla.org/en-US/docs/Glossary/Call_stack
- https://www.valentinog.com/blog/error/#what-is-an-error-in-programming
- https://stackoverflow.com/questions/3988788/what-is-a-stack-trace-and-how-can-i-use-it-to-debug-my-application-errors
If you'd like to hear more from me on error handling, I have also written about higher level error management strategies.