Handling Errors
By Tyler
To err is Human; to forgive, divine.
- Alexander Pope
Computers, being made by humans, are prone to errors. All the software on those computers, written by humans1, are also fallible. Error handling is an important topic in software development, and being able to properly use and interact with your language’s error systems, is an important step in becoming a competent software engineer.
Most languages provide means for dealing with errors, in one way or another2. However there are two main qualities that I think are important to any system: Transparency, and Decisiveness. In order to illustrate these qualities, I find it useful to compare how different languages (in this case Python, Go and Zig) approach (or fail to approach) them.
Transparency
Transparency is the quality of being able to look at code and understand what
kind of errors, or failure modes are possible. If function foo
may fail with
NoBarFoundError
, it should be easily and transparently delcared.
Python
Python deals with errors by throwing exceptions. Unfortunately it is impossible to declare anywhere in code that a function can fail, let alone how. Short of auditing every function call and subroutine, it is impossible to know all the failure modes of a function. Often possible errors and exceptions are declared in documentation strings, however the lack of transparency means that it is almost impossible to know all the ways a function can fail, due to the unkown nature of all the subroutines. This lack of transparency has two detrimental effects. First it lulls developers into a false comfort regarding stability of their code. Second it can lead developers to treat errors as an unimportant afterthought.
Go
Go errors are values. All fallible functions will return (at least) one value
of the error
type. If no error occurred the value will be nil
. This setup is
very transparent. If a function does not have a return value of the error
type,
then it can be considered “infallible”, all possible errors are handled at or
below that function. Otherwise we know that it could fail. Go also let’s you
create custom error types, allowing you to specifically indicate the failure mode
in the function declaration.
Zig
Zig also returns errors as values. However, in zig errors are always returned in union types, so that only one value is returned from a function. If you are unfamiliar with union types, think of it as an either/or type. The result can be either an expected value, or an error. Like in Go this means that any function that can fail can be seen by looking just at the return type.
Decisiveness
Errors should be decisive. Decisive means that an error should force a branching decision when they appear. Either no error has occured and we can continue on the “happy path”, or the error has occured and must be dealt with or crash the system. This is desirable because errors indicate that something has gone wrong and that our system (software) is now in a state that is unexpected. Being in an unexpected state should force a resolution or the system should fail, otherwise we risk the system behaving unexpectedly.
Python
Exceptions alter the flow of execution. As soon as an exception is thrown the system is on a direct route to crash, unless the exception is caught and handled. This is great. The one draw back is that because exceptions lack transparency, all of this occurs at runtime, meaning that crashing can be a frequent occurrence, if excpetions are not being caught and handled properly.
Go
Go errors are values, just like any other. The only language mechanism to force recognition of an error, is that you cannot assign two values to one. So, for example the following code would fail to compile.
// Fails to compile
val := FallibleFunc();
Instead the value must be assigned to it’s own variable.
val, err := FallibleFunc();
Beyond that however, there is nothing that stops a developer from doing nothing with the error.
And in fact the default line of code would be that the error is ignored. It requires the
discipline of the programmer to do an if (err != nil)
check every time an error could occur.
If that is forgotten, then the code will plod along silently ignoring the error.
Zig
Since Zig errors are values, one might expect it to have the same issues with decisiveness. However in zig, errors are special values. First they are unions, so in order to access the expected value of a function, you must provide a way for the code to act if there is an error there instead. Secondly, in the case that you are calling a function just for it’s side effects3, it is a compiler error to just throw away a value of the error union type. This means you always have to provide a line of execution that deals with the error. Even if your catch just ignores the error, at least then it was an explicit choice on the part of the developer.
So What?
So It’s probably pretty obvious that I think zig has done errors well. It meets both of my transparency and decisiveness requirments. Unfortunately, Zig is not stable, and so I don’t get to program in it in my day to day. Which then of the other two languages do I prefer?
On one hand, I really like Go because of the transparency. After using a transparent language, going back to one like python feels like you are flying blind. The peace of mind that comes with knowing that a function cannot fail is nice, and once you get used to it, it’s hard to go back to playing exception roulette.
That being said, Go’s system depends on both my discipline, and my trust in the discipline of other developers to always be checking the errors. I like to think that I’m good enough to always will do that, but I do make mistakes. I’m only human after all.