Why Go Won't Simplify Error Handling
James Reed
Infrastructure Engineer · Leapcell

Preface
“Error handling in Go is too verbose to write.” — This is a sentiment almost every Go programmer agrees with.
Recently, the Go team published an official blog post, formally announcing: they will no longer pursue any new proposals for error-handling syntax. This means that, moving forward, when writing Go code, you'll still be frequently typing the familiar line: if err != nil { return err }
.
This isn’t just the end of a syntactic sugar proposal — it’s a reaffirmation of the entire philosophy behind the language. So why did the Go team make this decision? And how should we interpret their persistence?
Three Attempts, Three Failures: The Journey of Go’s Error Handling Syntax
Over the past seven years, the Go team has tried three times to address the problem of “repetitive error handling” by introducing new syntax mechanisms. None of these efforts made it to adoption.
The First Attempt: The 2018 check
/ handle
Proposal
This proposal originated from the Go 2 draft. It was a complete syntactic change aimed to introduce:
check()
for capturingerror
from function calls;- A
handle
block for centralized error handling.
func printSum(a, b string) error { handle err { return err } // All check failures jump here x := check(strconv.Atoi(a)) y := check(strconv.Atoi(b)) fmt.Println("result:", x + y) return nil }
- 🔍 Pros: Clear structure, unified error path management.
- ⚠️ Cons: Introduces new syntax blocks, steepens the learning curve, increases semantic complexity, and sparked widespread controversy.
In the end, the Go team concluded the mechanism was too complicated and it never progressed beyond the draft stage.
The Second Attempt: The 2019 try()
Proposal
Learning from the first round, the Go team proposed a lighter version: the try()
function.
func printSum(a, b string) error { x := try(strconv.Atoi(a)) y := try(strconv.Atoi(b)) fmt.Println("result:", x + y) return nil }
Which is essentially equivalent to:
x, err := strconv.Atoi(a) if err != nil { return err }
- 🔍 Pros: Simple, clear, introduces no new syntax blocks — just a built-in function.
- ⚠️ Cons: The automatic
return
obscures control flow, violating Go’s long-standing emphasis on “explicit readability.” It makes debugging harder and increases cognitive load.
Eventually, this proposal was officially dropped due to strong opposition from the community.
The Third Attempt: The 2024 ?
Operator Proposal
This time, the Go team leaned into a more grounded design: inspired by Rust’s ?
, they proposed postfix syntactic sugar for error handling:
func printSum(a, b string) error { x := strconv.Atoi(a) ? y := strconv.Atoi(b) ? fmt.Println("result:", x + y) return nil }
Unfortunately, like the previous proposals, this one was quickly drowned out by various criticisms and a flood of personal-preference-based tweaks.
The Final Decision: No More Syntax-Level Changes
All three efforts failed to reach broad consensus. In June 2025, the Go team officially announced on their blog:
“For the foreseeable future, the Go team will stop pursuing syntactic language changes for error handling. We will also close all open and incoming proposals that concern themselves primarily with the syntax of error handling, without further investigation.”
This decision wasn’t due to a lack of technical solutions, but rather due to a combination of consensus gaps, cost-benefit analysis, and language philosophy.
It marks not just a pragmatic closure, but a reaffirmation of Go’s design principles.
Why Did the Go Team Stick to Their Guns? Two Core Reasons Explained
The Go team is not unaware of the fact that error handling is “repetitive to write,” and it’s not that they couldn't find a more concise syntax. Over the past seven years, they personally pushed three proposals, only to pull the plug on each one. This isn’t about technical limitations, but about value judgments.
We can understand why the Go team ultimately chose “no changes” from two perspectives:
1. No “Overwhelming Consensus” Was Reached
The Go team repeatedly emphasized, “We are not just looking for a working solution, but one that is widely accepted and used.”
But the reality was: every proposal sparked a lot of “this is not what I want” reactions:
- Some thought the
check/handle
approach was too complex; - Others felt the
try()
function was too automatic; - Some felt that the
?
operator, while intuitive, wasn’t clear enough in terms of semantics.
Every piece of syntactic sugar was met with style clashes, philosophical differences, and endless bikeshedding (pointless debates). The Go team clearly pointed out:
“Even the best proposals we’ve seen so far have failed to gain overwhelming support.”
Go’s design philosophy has always been very pragmatic: without consensus, there’s no change.
2. Technical Benefits vs. Costs Don’t Add Up
Behind every proposal, the Go team built prototype toolchains (including compilers, documentation, and other tools). However, they found that:
- Although the syntax looked more concise;
- Code may save a few lines;
- The cost of reading and understanding didn’t decrease proportionally.
For example:
x := strconv.Atoi(a)?
This line indeed omits the if
statement, but the control flow of the program becomes less explicit. It’s no longer clear how errors are returned or who handles them. This becomes a hidden part of the language’s logic. The Go team worries:
“The more magic we introduce at the language level, the higher the cost for users to debug, read, and track down problems.”
In their view, Go’s advantage has never been to write the least code, but to write code that is easy to understand, easy to debug, and stable to run.
Future Directions: No Syntax Change, but the Experience Can Be Improved
While the Go team has clearly stated that they will no longer pursue syntax-level changes for error handling, this doesn’t mean that error handling itself is “locked down.” On the contrary, there’s still plenty of room to improve the developer experience. The blog post mentioned several important directions for future work:
1. Reduce Redundant Code with Library Functions
The Go team has explicitly supported enhancing the standard library to reduce the repetition involved in error handling. For example:
x, err1 := strconv.Atoi(a) y, err2 := strconv.Atoi(b) if err := cmp.Or(err1, err2); err != nil { return err }
In this example, cmp.Or
is used to unify the handling of multiple errors, reducing repetitive checks. This approach maintains Go’s syntactic consistency while improving readability.
2. Strengthen IDE and Toolchain Support
A very pragmatic direction suggested in the blog is to make IDEs smarter at “hiding redundancy.”
Modern IDEs (especially when paired with LLM-based intelligent autocompletion) can significantly simplify the writing of repetitive code like if err != nil
. Future possibilities may include:
- Adding a toggle in IDEs to “hide error handling statements.”
- Expanding the
if err != nil
block only when necessary, improving reading flow. - Allowing AI to help generate more context-specific error messages.
This approach won’t change the language itself, but it could practically improve the efficiency of writing and reading Go code.
3. Focus on Error Handling Itself
Returning to the core of error handling, if we focus on the process of handling errors, rather than just returning errors, verbose code becomes less of an issue. Good error handling often requires adding extra information to the error. For example, a recurring comment in user surveys was the lack of stack trace information associated with errors. This can be addressed by generating and returning enhanced error-supporting functions, like:
func printSum(a, b string) error { x, err := strconv.Atoi(a) if err != nil { return fmt.Errorf("invalid integer: %q", a) } y, err := strconv.Atoi(b) if err != nil { return fmt.Errorf("invalid integer: %q", b) } fmt.Println("result:", x + y) return nil }
This approach not only adds clarity but also helps developers better understand the source of the error, leading to more maintainable and debuggable code.
Conclusion
Although the Go team has clearly stated that they will no longer pursue syntax-level changes for error handling, this doesn’t mean that the optimization of error handling is closed off. By enhancing the standard library, improving the toolchain, and focusing more on the context around error handling, developers can still improve the readability and efficiency of Go code while maintaining the consistency of the language.
This decision not only reflects Go’s commitment to explicitness and simplicity, but it also leaves room for future improvements in the tool ecosystem and developer experience.
We are Leapcell, your top choice for hosting Go projects.
Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:
Multi-Language Support
- Develop with Node.js, Python, Go, or Rust.
Deploy unlimited projects for free
- pay only for usage — no requests, no charges.
Unbeatable Cost Efficiency
- Pay-as-you-go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real-time metrics and logging for actionable insights.
Effortless Scalability and High Performance
- Auto-scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the Documentation!
Follow us on X: @LeapcellHQ