Errors are a key component of writing software - programs, libraries, scripts, you name it. We need to check for
them, catch them, mitigate, log, and finally create. In this article, I want to give you an overview of various methods
of that last activity - creating errors - sometimes also called raising or throwing, especially when the errors are
called exceptions. On that note...
Yeah, before we dive into the topic, let's make it clear: I'll use "error" and "exception" here almost
interchangeably. This is because I'm talking here about the abstract case rather than the thing that is used to
represent it. If you are coming from e.g., Java you may find this confusing, because these two names are used
differently. On the other hand if you are coming from e.g., Python you might be wondering why am I even writing this
paragraph.
Now then, as I was writing this article the classification of these methods evolved quite a few times. I doubt this
is the final form and depending on the feedback and my future endeavours I hope that I'll continue to make this list
better.
Returning the Error
Let's start with something dead simple. In this method we indicate an occurrence of the error simply by returning a
value from the function or the program. There are different ways of doing that mostly in terms of types and error
details.
Returning Boolean
I don't think it gets any easier than that. As long as you don't need to return any meaningful value from the
function and you only want to indicate whether the function passed or failed. In such case this will do. Just make the
function return a boolean that answers "Did the function pass?" or "Did the function fail?"
This method is sometimes used together with some bigger state. Especially if the executed function is a member of
some class. An example of such approach can be found in Qt.
Returning Error Codes
If you still don't need to return any meaningful value, but you want to differentiate between errors, you can encode
them with non-zero numbers. This time the value answers the question "What error occurred?" where zero means none.
The most common example of this approach is classic shell, where the result of the last command is stored inside the
$?
variable:
$ ls real_file
real_file
$ echo $?
0
$ ls does_not_exist
ls: cannot access 'does_not_exist': No such file or directory
$ echo $?
2
Interestingly, shells implement if statements where 0
is interpreted as positive case:
$ if ls real_file; then
> echo "True branch with $?"
> else
> echo "False branch with $?"
> fi
real_file
True branch with 0
Extreme example of throwing raw error codes at end-users is Windows and its API. I'd encourage you to avoid going to
such lengths.
Returning Error Objects
But you don't need to use numbers necessarily. The only requirement is that you remember about the ability to
represent all possible cases, including a situation in which no error occurred.
The Go programming language is cleverly using its core mechanics to deal with errors: tuples, nil values and
interfaces. Function that wants to raise an error should return object that fulfills a special error
interface that requires an Error() method to be present. In case the function does not want to report
anything, it can just return nil
instead. In simplified code it looks like this:
type TooLargeError int64
func (err TooLargeError) Error() string {
return fmt.Sprintf("For reason number is too large: %d", err)
}
func CheckNumber(value int64) error {
if value > 10 {
return TooLargeError(value)
}
return nil
}
func main() {
err := CheckNumber(4)
if err != nil {
fmt.Println(err)
}
err = CheckNumber(14)
if err != nil {
fmt.Println(err)
}
}
It is worth noting here the difference between shell and go errors in terms of boolean logic. Depending on your
style, e.g., prevalence of early returns, you may want to consider whether to assign positive or negative boolean value
to case in which error did not occur. Both are viable.
Returning an Invalid Value
What happens if you want to return a meaningful value from the same function?
In case of shell the return value is rarely used to store actual result, because that's the usual role of the
standard output stream. And in the above example of Go, the language has a very good built-in support for handling
tuples, so a function can just return a nilable error and the desired thing.
The approach of Go can be used in many other languages, with or without syntactic support, but what if you are
forced to return a single primitive object from the function?
Well, you can reverse the Error Codes approach by dedicating one or more from possible values to indicate errors
with them. Sometimes selecting those values can be straight-forward - for instance when the domain already has an
invalid space. Consider sizes which are usually represented with zero and positive integers, meaning if you use signed
integer as return value then you will have all of the negative numbers available to represent errors.
This is the approach used by read(3). When successful the function returns amount of bytes read,
but on error it returns -1
and sets a special global errno(3) to a value that describes what
exact error occurred:
char buffer[1024];
ssize_t bytes = read(fd, buffer, 1024);
if (bytes < 0)
perror("read()"); // Reads errno and prints description of error
else
do_something(buffer, bytes);
Note that I previously wrote that you can dedicate one or more values. Although I never found confirmation
in the POSIX standard, the only likely reason of read not using more negative numbers to indicate errors is to have
consistent interface to retrieve error details. Not all of the functions in the standard have enough available values
to indicate all the needed errors.
Anyway, sometimes you have enough values to use but you choose not to use them, and sometimes you may be forced to
use a single value. Sometimes you may even need to create your own constraints and rules in order to indicate an error.
An example of that is memory allocation with malloc(3) that returns NULL
in case of errors:
void* buffer = malloc(4096);
if (NULL == buffer)
perror("malloc()"); // In case of malloc it's always ENOMEM, really
free(buffer);
C and C++ standards (for NULL
and nullptr
) try very hard to define those two as null
pointer constants forcing compiler and platform implementations into guaranteeing that these will never point to any
real object and hopefully cause some segmentation faults here and there.
Returning Wrapped Values
Instead of bundling error with the value in tuple or some other container like Go did, you can wrap the value with an
object that will optionally indicate the error. This method may vary from simplified wrapper to a full-pledged monad.
Depending on where you end up on this spectrum the main difference will be the flow of error handling. You can use
tailored wrappers or something more generic like Either from Haskell or std::variant from C++.
A naive interface of tailored wrapper could look like this:
template<typename T, typename E=const char*>
struct Result {
Result(T value);
Result(T value, E message);
T m_value;
E m_message;
bool is_ok() const;
};
And used similarly to this:
Result<int> add_two(int value) {
if (value > 10)
return Result<int>(value, "i can't, it's too large");
return value + 2;
}
int main() {
for (int i = 8; i < 12; ++i) {
const auto number = add_two(i);
std::cout << i;
if (!number.is_ok())
std::cout << number.m_message;
else
std::cout << number.m_value;
std::cout << std::endl;
}
}
There is a very similar case to this one, but instead of value being wrapped, it contains a flag that indicates its
validity. This second approach is sometimes called zombie object. An example use of this approach would be
streams from C++ STL.
Implementations that are more on the monad-like side may allow user to bind functions to wrappers depending on their
state. This is very notably used in JavaScript's promises:
fetch("https://ignore.pl/example.json")
.then(response => response.json())
.then(console.log)
.catch(error => console.log("Error!", error);
Terminating the Process
In a scope of a single function we can use a technique called early return to finish the faulty execution.
For example, you could:
struct Message*
new_message() {
struct Message* msg = malloc(sizeof(struct Message));
if (NULL == msg)
return NULL;
const int res = initialize_message(msg);
if (-1 == res) {
free(msg);
return NULL;
}
return msg;
}
Without going deep into a discussion about whether early returns are good or bad (and I recall a few heated
discussions about it), you can already see that there is one already mentioned major flaw in it - it operates just on a
single level: in functions. Now, one way to overcome this limitation is going full nuclear.
When encountering a critical problem and operating in Unix-like environment you can simply terminate the process. In
order to show a distinct death condition you can use standard error stream or return code.
To do that you can use exit(3) in C, sys.exit in Python, exit or die in PHP, and other
equivalent functions in other languages. Some of them allow you to provide something to print out or return code, and
some don't. In C, you can often see:
noreturn void
panic(const char* fmt, ...) {
va_list args;
va_start(args, fmt);
vdprintf(2, fmt, args);
va_end(args);
exit(1);
}
This will format and print provided message to error stream and then terminate process returning 1
. Like
I mentioned earlier this is pretty much returning an error as a value and doing that earlier than a normal execution.
Thanks to the secondary output "lane" - the standard error stream we can provide the details of the error. This could be
compared to tuple solution from earlier to some extent.
Due to the fact that this method terminates the entire process it does not fit very well within bigger pieces of
software that rely a lot on their own interfaces and control flow. It shines when dealing with critical errors or when
working with a set of smaller programs that are running in shell environment.
Throwing Exceptions
When you want your program to be long-living and be able to recover from various failures terminating everything is
simply unacceptable and a different solution is needed. To be on the strict side of controlling the flow you may choose
to simply chain returning the error from the functions in the stack one by one. This is the path that e.g., Go chose.
The other way is a little more loose. It uses a secondary output lane to return the error and traverses the call-stack
until the error is handled. In a case that the error was not expected to be handled by developer it may fallback to
terminating process. The process of traversing the call-stack is usually called stack unwinding.
This method involves pushing errors into the second output lane - usually called throwing or
raising, and a way of limiting the unwinding and reading the pushed error - usually implemented by code blocks
or statements that are marked with a try keyword together with either catch or except.
When you need to raise errors of different severities and want to terminate some selected part of your execution
consider using exceptions.
Exceptions and exception-like interfaces are implemented in a wide selection of programming languages, for example in
Python:
def get(url, max_attempts=4):
attempts = 1
while attempts < max_attempts:
try:
return requests.get(url)
except HTTPError as err:
if err.response.status_code == 404:
raise
last = err
attempts += 1
raise RetryError from last
Or C++:
int
check_one(int x) {
if (x < 3)
throw "too little";
return x;
}
int
maybe_find(std::vector<int> numbers) {
int attempts_left = 3;
for (int i : numbers) {
try {
return check_one(i);
}
catch (const char* err) {
if (attempts_left > 0) {
attempts_left--;
continue;
}
break;
}
}
throw "not found";
}
There are a lot of flavours to the exceptions, but they generally tend towards the description I provided above. They
also usually use similar syntax with only small adjustments. Some of them, like Python, limit the objects that can be
raised as exceptions to classes derived from some base exception. Others, like C++ in the example above, let the user
throw anything they want.
Sometimes they are not syntactically implemented in the language, but instead they are implemented through functions,
consider Lua as an example:
function check_one(x)
if x < 3 then
error("too little")
end
return x
end
function maybe_find()
local attempts_left = 3
for _, i in pairs({1, 2, 3}) do
local ok, res = pcall(check_one, i)
if ok then
return res
end
if attempts_left > 0 then
attempts_left = attempts_left - 1
else
break
end
end
error("not found")
end
By wrapping a function call with pcall you get an additional return value that is a boolean that indicates
whether the function executed successfully or not. You also limit the propagation of errors created with error
within that protected call scope.
Signals
As a bonus, let's talk about POSIX signals. You won't see them being used too often for pure error handling, at
least not directly. They can be placed somewhere between terminating the process and exceptions as they allow
programmer to attempt a recovery, but are not very good at handling scopes and can have only one main entry point for
fault branch.
Signals can be also used by the operating system to report selected errors in execution, for example access to
invalid memory reference delivers SIGSEGV
. Consider an example:
sigjmp_buf env;
void
handle(int sig) {
siglongjmp(env, 1);
}
int
main(int argc, char* argv[]) {
signal(SIGSEGV, handle);
char* ptr = NULL;
if (sigsetjmp(env, 1))
ptr = malloc(1);
printf("%p\n", ptr);
*ptr = 10;
}
When compiled with all necessary includes and run, it will print out:
(nil)
0x555fb9a7c6b0
Of course, the second address may vary.
The problem with signals is that they require a good amount of attention. Especially when referencing sources over
the Internet. Even this example is not portable because it uses signal(2) and not sigaction(2).
Obviously, you are not limited to segmentation fault. You can use SIGABRT
with abort(3) or any
other signal.
Final Notes
Anything else? Probably yes. I tried to note similarities between the methods and mention some derivatives, but the
chance that I did not miss anything are rather thin. I think that there are some basic characteristics to be observed
among all (or some) of them.
With the common goal of reporting an error the first step is usually decoupling successful and failed execution
branches. One way involves creating values that are clearly defined as invalid and then dealing with them using usual
condition blocks (or statements). The other way involves jumping around the program or unwinding the stack.
The other step is describing the error to the user. This is optional, as in some cases the program or function is
answering a general question (e.g., "Did it fail?"). These details can be passed to the user via the actual return value
or some secondary output lane like: global variable, standard error output stream or throwing/raising.
This summary may sound obvious but I still think it is worthwhile to think about the reasons that are behind the
basic behaviours that we use each day. This is especially interesting from programming language perspective where these
days everything is pretty much the same. Maybe a simple change in some assumptions could start a breakthrough. Even if
not, then just practicing and gaining knowledge should be good enough of a reason to explore foundations.