One of the oldest and most persistent complaints about Go concerns the verbosity of error handling. We are all intimately (some may say painfully) familiar with this code pattern:
x, err := call()
if err != nil {
// handle err
}
The test if err != nil
can be so pervasive that it drowns out the rest of the code. This typically happens in programs that do a lot of API calls, and where handling errors is rudimentary and they are simply returned. Some programs end up with code that looks like this:
func printSum(a, b string) error {
x, err := strconv.Atoi(a)
if err != nil {
return err
}
y, err := strconv.Atoi(b)
if err != nil {
return err
}
fmt.Println("result:", x + y)
return nil
}
Of the ten lines of code in this function body, only four (the calls and the last two lines) appear to do real work. The remaining six lines come across as noise. The verbosity is real, and so it’s no wonder that complaints about error handling have topped our annual user surveys for years. (For a while, the lack of generics surpassed complaints about error handling, but now that Go supports generics, error handling is back on top.)
The Go team takes community feedback seriously, and so for many years now we have tried to come up with a solution for this problem, together with input from the Go community.
The first explicit attempt by the Go team dates back to 2018, when Russ Cox formally described the problem as part of what we called the Go 2 effort at that time. He outlined a possible solution based on a draft design by Marcel van Lohuizen. The design was based on a check
and handle
mechanism and was fairly comprehensive. The draft includes a detailed analysis of alternative solutions, including comparisons with approaches taken by other languages. If you’re wondering if your particular error handling idea was previously considered, read this document!
// printSum implementation using the proposed check/handle mechanism.
func printSum(a, b string) error {
handle err { return err }
x := check strconv.Atoi(a)
y := check strconv.Atoi(b)
fmt.Println("result:", x + y)
return nil
}
The check
and handle
approach was deemed too complicated and almost a year later, in 2019, we followed up with the much simplified and by now infamous try
proposal. It was based on the ideas of check
and handle
, but the check
pseudo-keyword became the try
built-in function and the handle
part was omitted. To explore the impact of the try
built-in, we wrote a simple tool (tryhard) that rewrites existing error handling code using try
. The proposal was argued over intensively, approaching 900 comments on the GitHub issue (#32437).
// printSum implementation using the proposed try mechanism.
func printSum(a, b string) error {
// use a defer statement to augment errors before returning
x := try(strconv.Atoi(a))
y := try(strconv.Atoi(b))
fmt.Println("result:", x + y)
return nil
}
However, try
affected control flow by returning from the enclosing function in case of an error, and did so from potentially deeply nested expressions, thus hiding this control flow from view. This made the proposal unpalatable to many, and despite significant investment into this proposal we decided to abandon this effort too. In retrospect it might have been better to introduce a new keyword, something that we could do now since we have fine-grained control over the language version via go.mod
files and file-specific directives. Restricting the use of try
to assignments and statements might have alleviated some of the other concerns. A recent proposal by Jimmy Frasche, which essentially goes back to the original check
and handle
design and addresses some of that design’s shortcomings, pursues that direction.
The repercussions of the try
proposal led to much soul searching including a series of blog posts by Russ Cox: “Thinking about the Go Proposal Process”. One conclusion was that we likely diminished our chances for a better outcome by presenting an almost fully baked proposal with little space for community feedback and a “threatening” implementation timeline. Per “Go Proposal Process: Large Changes”: “in retrospect, try
was a large enough change that the new design we published […] should have been a second draft design, not a proposal with an implementation timeline”. But irrespective of a possible process and communication failure in this case, the user sentiment towards the proposal was very strongly not in favor.
We didn’t have a better solution at that time and didn’t pursue syntax changes for error handling for several years. Plenty of people in the community were inspired, though, and we received a steady trickle of error handling proposals, many very similar to each other, some interesting, some incomprehensible, and some infeasible. To keep track of the expanding landscape, another year later, Ian Lance Taylor created an umbrella issue which summarizes the current state of proposed changes for improved error handling. A Go Wiki was created to collect related feedback, discussions, and articles. Independently, other people have started tracking all the many error handling proposals over the years. It’s amazing to see the sheer volume of them all, for instance in Sean K. H. Liao’s blog post on “go error handling proposals”.
The complaints about the verbosity of error handling persisted (see Go Developer Survey 2024 H1 Results), and so, after a series of increasingly refined Go team internal proposals, Ian Lance Taylor published “Reduce error handling boilerplate using ?
” in 2024. This time the idea was to borrow from a construct implemented in Rust, specifically the ?
operator. The hope was that by leaning on an existing mechanism using an established notation, and taking into account what we had learned over the years, we should be able to finally make some progress. In small informal user studies where programmers were shown Go code using ?
, the vast majority of participants correctly guessed the meaning of the code, which further convinced us to give it another shot. To be able to see the impact of the change, Ian wrote a tool that converts ordinary Go code into code that uses the proposed new syntax, and we also prototyped the feature in the compiler.
// printSum implementation using the proposed "?" statements.
func printSum(a, b string) error {
x := strconv.Atoi(a) ?
y := strconv.Atoi(b) ?
fmt.Println("result:", x + y)
return nil
}
Unfortunately, as with the other error handling ideas, this new proposal was also quickly overrun with comments and many suggestions for minor tweaks, often based on individual preferences. Ian closed the proposal and moved the content into a discussion to facilitate the conversation and to collect further feedback. A slightly modified version was received a bit more positively but broad support remained elusive.
After so many years of trying, with three full-fledged proposals by the Go team and literally hundreds (!) of community proposals, most of them variations on a theme, all of which failed to attract sufficient (let alone overwhelming) support, the question we now face is: how to proceed? Should we proceed at all?
We think not.
To be more precise, we should stop trying to solve the syntactic problem, at least for the foreseeable future. The proposal process provides justification for this decision:
The goal of the proposal process is to reach general consensus about the outcome in a timely manner. If proposal review cannot identify a general consensus in the discussion of the issue on the issue tracker, the usual result is that the proposal is declined.