Why Go Can't Try

2026-03-0214:035672niketpatel.com

Go's refusal to add a try keyword isn't about loving boilerplate.

Every Go developer has written this code thousands of times:

data, err := os.ReadFile(path)
if err != nil {
    return nil, err
}

And every Go developer has at some point looked at Zig or Rust and felt a pang of envy:

const data = try readFile(path);

One line. Clean. Honest. So why doesn't Go just do this? The common answer is "the Go team likes explicitness." That's true, but it's not the whole story. The real answer runs much deeper.

Zig Is Actually More Explicit Than Go

Here's the irony nobody talks about: Zig is more explicit about errors than Go, not less.

In Zig, a function's return type tells you everything before you read a single line of its body. !Config means this function can fail. The compiler knows every possible error it can return. If you don't handle a case, it won't compile. try isn't hiding anything, it's saying clearly "I see this can fail, and I'm intentionally propagating it upward."

Go's if err != nil, on the other hand, is not actually enforced by the language. This compiles just fine:

data, _ := os.ReadFile(path)

So Go's celebrated explicitness is partly an illusion. The pattern is verbose but the compiler doesn't require you to actually handle anything. Zig's compiler does. On the question of which language truly enforces explicit error handling, Zig wins.

So Why Not Just Add try?

At first glance it seems simple. try isn't a keyword in Go today, so adding it wouldn't break existing code. A function like this:

func loadConfig(path string) (Config, error) {
    data   := try os.ReadFile(path)
    raw    := try parseJSON(data)
    config := try validate(raw)
    return config, nil
}

looks like a clear improvement. Less noise, same semantics. The Go team's official objection is that try creates invisible exit points: every try call is a potential early return that you can miss when reading code quickly. They've historically drawn a line between this and exceptions, arguing both make control flow hard to follow.

It's a reasonable point. But it's not the real reason try hasn't landed.

The Real Problem Is Deeper: Go's Error Type

To understand why, you have to look at what error actually is in Go:

type error interface {
    Error() string
}

That's the entire definition. Any type with an Error() string method is a valid error. This means errors in Go carry arbitrary unstructured information, every package invents its own error types, and the compiler has absolutely no idea what errors a function might return. The ecosystem built on top of this (errors.Is(), errors.As(), fmt.Errorf("%w")) is entirely a set of runtime conventions, not compile-time guarantees.

Zig's error sets are the opposite. They are finite, compiler-known, and exhaustively checkable:

const ConfigError = error{ FileNotFound, ParseFailed, InvalidInput };

The compiler can infer the union of all possible errors across an entire call chain automatically. It can tell you when you've missed a case. This is a fundamentally different and more powerful model.

That power comes with a hard constraint. A Zig error is not a struct or pointer: it is a globally unique, 16-bit integer. The compiler maps every error.FileNotFound across your codebase and every library you depend on to the same integer ID. Zero overhead, zero heap allocations.

But because it's just a number, it cannot carry a payload. Go makes it trivial to attach context to a failing call:

func loadEverything() error {
    if err := loadConfig(); err != nil {
        return fmt.Errorf("failed to load config: %w", err)
    }
    if err := loadData(); err != nil {
        return fmt.Errorf("failed to load data: %w", err)
    }
    return nil
}

In Zig, there's no equivalent. If both calls fail with error.FileNotFound, the error value alone can't tell you which file was missing. Zig's answer is the Error Return Trace: instead of enriching the error value, the compiler tracks the error's path automatically. In Debug or ReleaseSafe mode, an unhandled error points you directly to the failing try:

error: FileNotFound
/src/main.zig:8:5: 0x104b2a61b in loadEverything (main)
    try loadConfig();
    ^
/src/main.zig:2:5: 0x104b2a5d7 in main (main)
    try loadEverything();
    ^

It tells you where the error traveled, not what it means. Rather than enriching the error value, Zig enriches the tooling.

Here's the uncomfortable truth: a try keyword in Go without fixing the error type is just syntax sugar. You'd get slightly less typing but none of the real benefits - no exhaustiveness checking, no compiler-inferred error sets, no guarantee that you've actually handled every case. It would make control flow less visible while delivering only a fraction of what makes Zig's approach genuinely good.

Why Fixing the Error Type Is Impossible

So why not fix error too? Because it would break everything.

Go's error interface is not just in the standard library - it is the standard library. Every function in os, io, net, database/sql, and the rest returns this interface. Millions of lines of production Go code are built around the assumption that errors are opaque interface values you inspect at runtime.

If Go introduced typed error sets tomorrow, os.ReadFile would need to return something like os.PathError | io.EOF instead of plain error. Every single caller in existence would break. All the errors.Is() and errors.As() machinery would become either redundant or need a complete redesign. Third-party libraries would be split between old and new styles for a decade. The standard library itself would feel like a legacy API overnight.

This isn't a small migration. This is rewriting the foundation of the language while the building is occupied.

What the Go Team Is Really Saying

When the Go team argues against try on explicitness grounds, I think they're defending a deeper position they rarely state directly:

You can't get the real benefits of Zig-style error handling without a fundamental redesign of the error type. A redesign of the error type breaks every Go program ever written. Therefore, try syntax alone is a half-measure that costs you readability while giving you little of real value in return.

That's actually a coherent and defensible position. It's just not the argument they usually make publicly.

The Honest Takeaway

Go and Zig made different foundational choices about errors early on, and those choices compound over time. Zig built errors into the type system from the start: exhaustive, zero-cost, but incapable of carrying context. Go made them a flexible interface: expressive and wrappable, but with no compiler enforcement. Both decisions made sense at the time. But they lead to very different places.

Go isn't refusing try because its designers love boilerplate. It's refusing try because the one change that would make it genuinely worthwhile; a typed, compiler-aware error system is the one change it can never make.

The if err != nil pattern isn't going anywhere. Not because Go can't add a keyword, but because the error system underneath it is too deeply woven into everything Go has ever built to change now.

That's the real reason Go can't try.


Read the original article

Comments

  • By chis 2026-03-0218:374 reply

    These AI written articles carry all the features and appearance of a well reasoned, logical article. But if you actually pause to think through what they're saying the conclusions make no sense.

    In this case no, it's not the case that go can't add a "try" keyword because its errors are unstructured and contain arbitrary strings. That's how Python works already. Go hasn't added try because they want to force errors to be handled explicitly and locally.

    • By win311fwg 2026-03-0218:511 reply

      It is simpler than that. Go hasn't added "try" because, much like generics for a long time, nobody has figured out how to do it sensibly yet. Every proposal, of which there have been many, have all had gaping holes. Some of the proposals have gotten as far as being implemented in a trying-it-out capacity, but even they fell down to scrutiny once people started trying to use it in the real world.

      Once someone figures it out, they will come. The Go team has expressed wanting it.

      • By WalterGR 2026-03-0219:14

        > nobody has figured out how to do it sensibly yet.

        In general or specifically in Go?

    • By qezz 2026-03-0218:43

      The mentioned in the article `try` syntax doesn't actually make things less explicit in terms of error handling. Zig has `try` and the error handling is still very much explicit. Rust has `?`, same story.

    • By jcmfernandes 2026-03-0218:54

      I just read the article and I didn't get away with that rationale. Now, this isn't to say that I agree with the author. I don't see why go would *have* to add typed error sets to have a try keyword.

      Yes, mimicking Zig's error handling mechanics in go is very much impossible at this point, but I don't see why we can't have a flavor of said mechanics.

    • By LovelyButterfly 2026-03-035:16

      What led you to believe this is an AI written article?

  • By seethishat 2026-03-0218:003 reply

    The programmer is explicitly throwing away the error returned by ReadFile (using the underscore) in the criticism of Go.

        data, _ := os.ReadFile(path)
    
    Saying that is not explicit is just wrong.

    • By n_u 2026-03-0218:393 reply

      I think the argument is that the compiler does not enforce that the error must be checked. It's just a convention. Because you know Go, you know it's convention for the second return value to be an error. But if you don't know Go, it's just an underscore.

      In a language like Rust, if the return type is `Result<MyDataType, MyErrorType>`, the caller cannot access the `MyDataType` without using some code that acknowledges there might be an error (match, if let, unwrap etc.). It literally won't compile.

      • By sethops1 2026-03-0219:052 reply

        When you see .unwrap in Rust code, you know it smells bad. When you see x, _ := in Go code, you know it smells bad.

        > But if you don't know Go, it's just an underscore.

        And if you don't know rust, .unwrap is just a getter method.

        • By n_u 2026-03-0219:31

          One big difference is that with unwrap in Rust, if there is an error, your program will panic. Whereas in Go if you use the data without checking the err, your program will miss the error and will use garbage data. Fail fast vs fail silently.

          But I'm just explaining the argument as I understand it to the commenter who asked. I'm not saying it is right. They have tradeoffs and perhaps you prefer Go's tradeoffs.

        • By xigoi 2026-03-0219:401 reply

          > When you see x, _ := in Go code, you know it smells bad.

          What if it’s a function that returns the coordinates of a vector and you don’t care about the y coordinate?

          • By yndoendo 2026-03-0221:32

            Haven't jumped into rust for a while. Had to read up on what .unwrap() does.

               x, _ := 
            
            With the topic of .unwrap() _ is referencing an ignored error. Better laid out as:

              func ParseStringToBase10i32AndIDoNotCare(s string) {
                 i, _ := strconv.ParseInt(s, 10, 32)
                 return i
              }
            
            Un-handled errors in Go keeps the application going were rust crashes .unwrap().

            Ignoring an output data value or set is just fine. Don't always need the key and value of a map. Nor a y axes in vector<x,y,z> math.

      • By AnimalMuppet 2026-03-0219:211 reply

        Go has tools for checking things like this. It's just not in the compiler. If you don't want to enforce that all errors are checked, go doesn't force you to. If you do, it requires you to run an extra tool in your build process.

        (Or in your commit hook. If you want to develop without worrying about such things, and then clean it up before checkin, that's a development approach that go is perfectly fine with.)

        • By qezz 2026-03-0220:29

          > requires you to run an extra tool

          And the more I work with Go, the less I understand why warnings were not added to the compiler. Essentially instead of having them in the compiler itself, one needs to run a tool, which will have much smaller user base.

          But anyway, in Go, it's sometimes fine to have both non-nil error and a result, e.g. the notorious EOF error.

      • By maccard 2026-03-0218:471 reply

        > if the return type is `Result<MyDataType, MyErrorType>`, the caller cannot access the `MyDataType` without using some code that acknowledges there might be an error (match, if let, unwrap etc.)

        I think you can make the same argument here - rust provides unwrap and if you don’t know go, that’s just how you get the value out of the Result Type.

        • By qezz 2026-03-0220:241 reply

          The big difference is that with `(T, error)` as a return type, any value on the caller side will look like a valid one (thanks to zero values).

            a, err := f()
            // whether you forgot to handle the `err` or not, 
            // the `a` carries a zero value, or some other value.
          
          In rust it's not the case, as the `T` in `Result<T, E>` won't be constructed in case of an error.

          • By zimpenfish 2026-03-0623:29

            > "the `a` carries a zero value, or some other value."

            Or you could return pointers and use `nil` in the error case. Bonus is that it'll then panic if you try to use it without checking the error.

            (Yes, I know, it makes everything else a faff and is a silly idea.)

    • By gopher_space 2026-03-0219:28

      Criticisms of Go seem like they pivot on the author's understanding of what's being done with `_` and sometimes `nil`. It's a strongly-typed language with a lot of flexibility around type, and that's nice to have when working on edge systems like a data ingester.

    • By jiehong 2026-03-032:48

      How about:

          file.Close()
      
      Did you see the non handled error here? The compiler doesn’t care.

  • By cryptos 2026-03-0217:014 reply

    My takeaway is that Go almost always prefers simplicity and not so much good software engineering. `nil` without compiler checks is another example, or designing a new language without generics. However the overall simplicity has its own value.

    • By sheept 2026-03-0218:132 reply

      I agree, its strength (beyond goroutines) is that anyone who knows one of the popular languages (Python, Java, etc) can easily translate their idioms and data structures to Go, and the code would remain easy to read even without much Go experience. That's probably one reason why the TypeScript compiler team chose Go.

      But this makes the language feel like Python, in some ways. Besides nil, the lack of expressivity in its expressions makes it more idiomatic to write things imperatively with for loops and appending to slices instead of mapping over the slice. Its structurally typed interfaces feel more like an explicit form of duck typing.

      Also, Go has generics now, finally.

      • By saynay 2026-03-0218:37

        > But this makes the language feel like Python

        From what I remember of a presentation they had on how and why the made Go, this is no coincidence. They had a lot of Python glue code at Google, but had issues running it in production due to mismatched library dependencies, typing bugs, etc. So they made Go to be easy to adopt their Python code to (and especially get the people writing that code to switch), while addressing the specific production issues they faced.

      • By andriy_koval 2026-03-0218:211 reply

        > he popular languages (Python, Java, etc) can easily translate their idioms and data structures to Go, and the code would remain easy to read even without much Go experience

        disagree, they made many decisions which are different from mainstream: OOP, syntax as examples.

        • By sheept 2026-03-0218:301 reply

          Sure, the syntax is unique, but it's fairly easy to get over that. I guess I'm comparing to Rust, where not only is syntax different, but data structures like a tree with parent references aren't as straightforward (nor idiomatic), and there's a lot more explicit methods that requires knowing which are important and which are just noise (e.g. unwrap, as_ref).

          I would argue that after a short tutorial on basic syntax, it's easier for a Python/JavaScript programmer to understand Go code than Rust.

          • By andriy_koval 2026-03-0218:381 reply

            to me Rust syntax is less alienating, they adapted ML syntax which is probably second most popular(scala, typescript, kotlin) after C style syntax, while Go from whatever reasons got something totally new.

            • By assbuttbuttass 2026-03-0220:511 reply

              Which aspects of Rust syntax are adapted from ML? Semantics sure, but to me the syntax seems a lot more similar to C++ (e.g. semicolons, type parameters using <>, etc.)

              • By andriy_koval 2026-03-0221:02

                mostly how you declare var + type, and function with parameters, which is probably majority of code boilerplate.

    • By marcosdumay 2026-03-0218:45

      It's a very improved 1960s language.

      For some uses, that's all you need, and having more features often detract from your experience. But I'm doubtful on how often exactly, I have been able to carve out a simple sub-language that is easier to use than go from every stack that I've tried.

    • By antonvs 2026-03-0218:15

      I think of it as a bit like Python with stronger types.

      I'm not convinced that you couldn't have good software engineering support and simplicity in the same language. But for a variety of mostly non-technical reasons, no mainstream language provides that combination, forcing developers to make the tradeoff that they perceive as suiting them best.

    • By ratrace 2026-03-0312:22

      [dead]

HackerNews