err := db.Get(&latLong, "SELECT lat, long FROM cities WHERE name = $1", name)
if err == nil {
return latLong, nil
}
latLong, err = fetchLatLong(name)
if err != nil {
return nil, err
}
err = insertCity(db, name, *latLong)
if err != nil {
return nil, err
}
In Rust propagating errors is a lot more succinct and easy to do. It is usually what you want to do as well (you can think of Python and C++ exceptions as essentially propagating errors). The special case can be handled explicitly. In Go, you have to handle everything explicitly, and if you don't you can fail catastrophically.I guess it comes down to what features the language provides that makes it easy to do "the right thing" (where "the right thing" may depend on where your values lie; for example, I value correctness, readability of domain logic, easy debugging etc). And in my opinion, it's easy to do what I consider bad software engineering in languages like Go.
val, err := someOperation(arg)
if err != nil {
return nil, fmt.Errorf("some operation with arg %v: %v", arg, err)
}
It really sucks when you're debugging an application, and the only information you have is "read failed" because you just propagated errors. Where did the read happen? In what context?Go errors usually contain enough context that they're good enough to print to console in CLI applications. Docker does exactly that - you've seen one if you've ever tried executing it as a user that isn't in "docker" group:
docker: Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post http://%2Fvar%2Frun%2Fdocker.sock/v1.35/containers/create: dial unix /var/run/docker.sock: connect: permission denied. See 'docker run --help'.Not to pick on any of these projects, but this pattern is way too common to not have a some sugar:
https://github.com/goodlang/good/blob/3780b8d17edf14988777d3...
https://github.com/kubernetes/kubernetes/blob/1020678366f741...
https://github.com/golang/go/blob/6cf6067d4eb20dfb3d31c0a8cc...
Yes, because each level is (hopefully) there for a reason, and provides a deeper context about where and why the error occurred.
If your contextual information is too repetitive and redundant, it may be the time to refactor your code and remove the unnecessary layers.
It reminds me of A Philosophy of Software Design:
The utility of a module (or any form of encapsulation such as a function) is greater, the smaller the interface relative to the implementation. Shallow ve deep modules.
Error handling might be a proxy to determine the semantic depth of a module.
Another perspective: (real) errors typically occur when a program interacts with the outside world AKA side effects etc.
Perhaps it’s more useful to lift effects up instead of burying them. That will automatically put errors up the stack, where you actually want to handle them.
This perspective also made me think of another advantage of Go error handling over traditional try/catch logging:
In other programming languages, when doing some asynchronous work in some background worker or whatever, exceptions become useless as error reporting mechanisms, because stack traces don't correspond to the logical flow of the program. I remember Java's Spring to be exceptionally painful in this regard.
In Go, since errors are values and you're expected to add context information whenever appropriate, you can shift and shuffle those errors between stacks, and still keep useful context information when you eventually read those errors.
some_operation(&arg)
.with_context(|| format!("Some operation failed: {arg}"))?;
or, in most cases, the shorter some_operation(&arg)
.context("Some operation failed")?;
If you forgot to call `with_context` and simply write some_operation(&arg)?;
the error is still propagated, it's just missing whatever context you wanted to add.Whether that's better or worse than the Go snippet is something I'll let the reader decide.
The only issue there is if you want to return the error with code 200 (which you shouldn't, but it's been known to happen). In that case the Go code and the Rust code will look a bit closer to each other because then you can't use `?` this way (without writing some more boilerplate elsewhere).