Question Mark Operator Petition

99% of my Go code is error checks. A Rust style question mark operator would significantly improve readability.

This can be done idiomatically. Where Rust manipulates monadic types (especially Result), Go manipulates multivalue returns.

The return arity of ? in Go should reduce by one compared to a function call without applying the operator. Automatically return zero values, and the error value, as in a macro, when encountering a failure. Elide error value assignment within the function, when the operation succeeds.

Multiple question mark expressions can be chained together, particularly for network and file operations, where every dang method can fail.

How would you wrap an error with additional context in your proposal? You can’t .map_err() or whatever.

1 Like

Go’s main idea not to hide errors handling behind fancy sugar of control flow. ? doesn’t solve anything, since the go style is not chaining as in rust. Go prefers to handle things explicitly and it’s one point. Rust likes kilometers of chained methods around one thing before you can actually get what you want. It’s not a question of readability or simplicity, it’s how things are being done in this language. There were tons of different suggestions and discussions around alternatives which didn’t find support of the community.

6 Likes

Yep. This has also been discussed a lot, including recently on this forum:

I linked to an issue and discussion on that thread:

You can read through it all. The upshot is: I don’t think this will be adopted ever because gophers don’t want it for the most part. The go team and developers in the ecosystem are pretty slow to change. That is a good thing and a bad thing.

Many years ago I read The Secrets of Consulting by Weinberg (excellent book btw). In it he has a rule called “The Fast-Food Fallacy” and it always stuck with me.

No difference plus no difference plus no difference plus. . . eventually equals a clear difference.

I’m not saying we should never evolve go (we added generics for example and that has been fine). But I’m OK with being careful about how we evolve it. Change too much and the thing that originally made it a useful tool might get lost. Right now, I think one of the most useful aspects of go is that it is extremely easy to reason about go code. At the expense of brevity. For me, that tradeoff is working really well.

5 Likes

99%? Then you must be doing something wrong. Can you share an example project where 99% of your code is error checks? Most of my code looks like this:

// FindAvailableRegions retrieves available serverless regions from the DevOps API.
//
// Example - get all regions:
//
//	admin := client.Admin()
//	regions, err := admin.FindAvailableRegions(ctx)
//
// Example - filter by organization access:
//
//	regions, err := admin.FindAvailableRegions(ctx,
//	    options.FindAvailableRegions().SetFilterByOrg(options.FilterByOrgEnabled))
func (a *Admin) FindAvailableRegions(ctx context.Context, opts ...options.Builder[options.FindAvailableRegionsOptions]) ([]Region, error) {
	// Merge options
	merged, err := options.MergeOptions(opts...)
	if err != nil {
		return nil, err
	}

	// Build command with query parameters.
	// Hard-coding to region-type=vector because classic isn't relevant to this client.
	cmd := a.createCommand(http.MethodGet, "/regions/serverless", nil).
		withQueryParam("region-type", "vector")
	if merged != nil {
		if merged.FilterByOrg != nil && *merged.FilterByOrg {
			cmd.withQueryParam("filter-by-org", "enabled")
		}
	}

	// Execute request
	resp, err := cmd.execute(ctx)
	if err != nil {
		return nil, err
	}

	// Parse response - the API returns a JSON array of regions
	var regions []Region
	if err := json.Unmarshal(resp.Body, &regions); err != nil {
		return nil, fmt.Errorf("failed to parse regions response: %w", err)
	}

	return regions, nil
}

Operations can fail. At times I might return errors directly. At times I might add more context. But at each stage where things can fail, it’s obvious what is happening. I feel like anybody who can read code in any language can really easily reason about what is happening here.

If you compare this to ecosystems that rely heavily on magic errors, it can often be hard to reason about where/how the error will be handled. I recently was working on a .NET project (and this is not me throwing shade at C#/.NET; I cut my teeth in that ecosystem and it is great and Anders is absolutely brilliant). It is really nice to just write your code and never care about what is throwing where. Until you have to answer the question “what exactly happens when something goes wrong here?”.

Go’s super clear (if a bit expressive) way of handling things works well in my opinion. If you are finding you are not getting a lot of value from this tool, try a different one!

3 Likes

The thing about Go is that it is not Rust, just like it is not Java, or Python, or any other of now hundreds of languages available. I love the way Go does things. I love that it does things differently. I love that the Go team is almost as conservative about language features as the ANSI C committee. In the era of ever moving targets and pervasive bloat, status quo and moderation brings me peace of mind.

So please, let’s keep @#$? for when a true innovation is on the horizon. If there is any place where I’d rather see new syntax, it is struct tags or perhaps iterators. That said, struct tags already bring in enough magic behavior. While I like the Kong package, it serves for an example of how to quickly abuse them. Iterators are only ugly when you write them, not when you use them.

4 Likes

Ten nested Elvises shouldn’t pollute the logs by wrapping the original error message 10x.

Any custom message wrapping could still be performed, carefully, manually.

? is passthrough.

Go’s explicit error handling hides my code. The vast majority of my code is boilerplate.

Sure, just never conduct any I/O operations. Host an entire operating system in memory.

What does that have to do with error handling syntax? This is a complete non sequitur. It’s clear that, once again, you are coming here to complain without any valid points. And then you escalate with increasingly ridiculous statements that have nothing to do with the original topic. Are you OK, Andrew?

For fun, I ported that example to the playground. Readers can decide which version seems clearer (if any):

Also - you want to know what’s funny? If you take the playground annotations for file names and imports out, this version is fewer lines of code. Error handling and all. Have a great day, everybody. :slight_smile:

2 Likes

Well, there is a skill to read between the lines. I really don’t understand where I/O concern is coming from. I have projects where I do millions of I/O operations and use wrappers to hide implementation in one place and call it where I need. Probably there is also a skill issue in place with code arrangements. Dunno, try to read Go best practices. Don’t write kilometer long functions with lot’s of logic inside. Decorate your code with wrappers. 100500 of ways to reduce the number of if err != nil but people are still complaining about it in 2026. Circus.

4 Likes

Go is an opinionated language - it doesn’t want to support a hundred different ways to write code differently. And error handling is one point, where it is verbose with a purpose. In the idea of idiomatic go code you should think about errors and handle each one accordingly. A clear paradigm shift versus just throw and forget.

There can be a lot of reasons why an operation fails and depending on the reason the lower layers can already handle a lot of cases sensibly, instead of just throwing the error. And if an error is thrown, it should include clear context and always a clear call to action for the end-user. Too many UIs just display “oops, something went wrong” - what should I do now? Try again, call customer service, change something in my data, …?

Clear error handling goes a long way to reduce support and bugfixing overhead. As a Dev-Ops Engineer myself, I use very very few simple if err != nil return err - I almost always include a clear error description, context and often an error type. And in many cases I also do something else like a retry or fixing of input data and so on.

If you are disparaged by the many lines which distract from your source code, there are plugins available for popular IDEs, which will fold or hide error handling blocks, so they are less visible and will also generate them with a simple key combination automatically. So if you want to minimize your interaction with error-handling code, you can easily arrive at a configuration where you neither have to write nor read them, but they still exist.

4 Likes

yesterday, I came across a very interesting project. It is like a “transpiler” for Go and offers some good things to the language. It is not a new language, because it’s only go…The link is https://dingolang.com or GitHub - MadAppGang/dingo: A meta-language for Go that adds Result types, error propagation (?), and pattern matching while maintaining 100% Go ecosystem compatibility

3 Likes