Whenever someone posts anything related to the Go programming language on Hacker News, it never takes long before someone complains about error handling. I find it interesting because it is exactly one of things I like the most about Go.
I don’t want to specifically talk about error handling though. I want to talk about a feature that is intrinsically tied to it in Go: the ability of functions to return multiple values
For instance, in Go it is common and idiomatic to write functions like this —
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0.0, errors.New("divide by zero")
}
return a / b, nil
}
So the caller would do:
result, err := Divide(x, y)
if err != nil {
// do error handling...
}
Some people deplore this. I absolutely love it. I find it so much clearer than, for instance, what we often have to do in C#. You see, C# didn’t have multiple returns (until very recently; see below) so you ended up with a few options.
First, you can simple throw exceptions.
public SomeObject GetObjectById(int id) {
if (!SomeObjectRepo.Has(id))
throw new ArgumentOutOfRangeException(nameof(id));
// ...
}
...
try
{
var obj = GetObjectById(1);
// do something with obj
}
catch (ArgumentOutOfRangeException ex)
{
// error handling
}
I find the flow difficult to read. Particularly because variables are scoped within the try-catch so often you need to first declare something above the try
and then test it after the catch
.
A second option is to return null
:
public SomeObject GetObjectById(int id)
{
if (!SomeObjectRepo.Has(id))
return null;
// go get the object
}
...
var obj = GetObjectById(1);
if (obj == null)
{
// do error handling
}
This looks closer to what I like but it still has some serious downsides. You don’t get any error information. What made it fail? I don’t know. As well, this doesn’t work for non-nullable types. A method returning a, say, int
cannot return null
. Sure, you could return int?
instead of int
and then test for .HasValue
but that’s cumbersome and artificial.
A third option is the use of a generic return type. Something like —
public class Result<T>
{
public T Value {get;protected set;}
public Exception Exception {get; protected set;}
public bool IsError => Exception != null;
public Result() : this(default(T)) {}
public Result(T value)
{
Value = value;
}
public static Result<T> MakeError(Exception exception)
{
return new Result<T>
{
Value = default(T),
Exception = exception
};
}
}
You could then use this to return values like —
public Result<int> Divide(int a, int b)
{
if (b == 0)
{
return Result<int>.MakeError(new DivideByZeroException());
}
return new Result<int>(a / b);
}
...
var res = Divide(8, 4);
if (res.IsError)
{
// do error handling, e.g.
throw res.Exception;
}
// do something with res.Value (2)
This works, but it looks artificial. You need to create instances of Result<T>
all around all the time. It is not that bad if your codebase uses this throughout and it becomes automatic for all programmers envolved. When it’s an exception to the rule, it is horrible.
A very similar solution is to return something like Tuple<T1, T2, ...>
—
public Tuple<int,Exception> Divide(int a, int b)
{
if (b == 0)
return new Tuple<int,Exception>(0, new DivideByZeroException());
return new Tuple<int,Exception>(a/b, null);
}
...
var res = Divide(1, 2);
if (res.Item2 != null) // Item2 is the exception
{
// do error handling
}
// do something with res.Item1
Same principle. It’s ugly and artificial, but it will come back to us.
The way the C# authors found to work around this problem is the idiomatic try-pattern, which consists in creating non-exception-throwing versions of methods. For example, if we go back to the first C# example above (GetObjectById()
), we could create a second method like so —
public bool TryGetObjectById(int id, out SomeObject result) {
try
{
result = GetObjectById(id);
return true;
}
catch
{
result = default(SomeObject);
return false;
}
}
...
SomeObject result;
if (!TryGetObjectById(1, out result))
{
// do error handling
}
// do something with result
Note that ever since C# 7.0 you can declare the out
variable directly inside the method call as such —
if (!TryGetObjectById(1, out var result))
Which spares you of declaring your out
variables arguably at the expense of clarity.
This method is idiomatic and found everywhere in the .NET Framework. I actually like it but it still has the problem of losing important information, namely what caused the method to fail: all you get is true
or false
.
In C# 7.0, the language authors came up with a new solution: they added syntactic sugar to the language to make the tuple solution a bit more appealing —
public (int, Exception) Divide(int a, int b)
{
if (b == 0)
return (0, new DivideByZeroException());
return (a / b, null);
}
...
var (res, err) = Divide(1, 2);
if (err != null)
{
// do error handling
}
Suddenly this becomes very familiar to a Go programmer. In the background, this is using a tuple. In fact, you can check that this is so by using the method above like this —
var res = Divide(1, 2);
if (res.Item2 != null)
// do error handling
// use res.Item1
You will see that res
is of type System.ValueTuple
. Also, if you create a library in C# 7.0 and then try to use it with a program in older versions of C#, you will see that the exposed type of the method is a tuple. This is actually nice because it means this big language change is backwards compatible.
All that said, I haven’t seen many uses of the new tuple returns in C# code in the wild. Maybe it’s just early (C# 7.0 has been out for only a few months.) Or maybe the try-pattern is simply way too ingrained in the way of doing things in C#. It’s more idiomatic.
I sure prefer the new (Go-like) way.