Repository: go
Issue: proposal: spec: cleaner error handling happy path via limited pattern matching
Labels: LanguageChange,Proposal,error-handling,LanguageChangeReview

### Go Programming Experience

Experienced

### Other Languages Experience

JavaScript, Elixir, Kotlin, Dart, Ruby

### Related Idea

- [ ] Has this idea, or one like it, been proposed before?
- [X] Does this affect error handling?
- [ ] Is this about generics?
- [ ] Is this change backward compatible? Breaking the Go 1 compatibility guarantee is a large cost and requires a large benefit

### Has this idea, or one like it, been proposed before?

I don't think so, but considering how many error handling proposals there have been over the years it is possible.

### Does this affect error handling?

Yes. It differs in that it doesn't attempt to handle errors in a magical way, but instead introduces a new syntax that can be used for several different types of common data handling.

### Is this about generics?

No.

### Proposal

### Prior Art

In Elixir, pattern matching is often used as a way to deal with errors. Functions that can return error values usually return a tuple of the form `{:error, error_string}`, and will usually return `{:ok, result}` when they don't fail. This results in code that looks like

```elixir
{:ok, result} = thing_that_can_fail()
```

That code checks to make sure that the first element is `:ok` and assigns the second element to a new variable, `result`. If the pattern does not match, it will crash. When you want to handle the error, however, you generally use something like a `case` expression, which allows checking multiple possible patterns:

```elixir
case thing_that_can_fail() do
  {:ok, result} -> use_result(result)
  {:error, err} -> handle_error(err)
end
```

This is fine until you need to do a bunch of things in a row and several of them can fail:

```elixir
case thing_that_can_fail() do
  {:ok, result} -> case other_thing_that_can_fail() do
                              {:ok, result2} -> use_results(result, result2)
                              {:error, err} -> handle_error(err)
                            end
  {:error, err} -> handle_error(err)
end
```

To help deal with this, Elixir has a `with` expression which allows handling multiple pattern matches in a row. The following code is functionally identical to the previous snippet:

```elixir
with {:ok, result} <- thing_that_can_fail(),
         {:ok, result2} <- other_thing_that_can_fail() do
           use_results(result, result2)
else
  {:error, err} -> handle_error(err)
end
```

### Proposal

My proposal is to adopt a variant of the second syntax above, `with`, and make it a bit more Go-like, and use it to allow deduplication of repeated, nearly identical error handling. This would work by adding a new keyword, though what exactly I'm not sure. To illustrate, I'll use `with` to match the Elixir code, but it doesn't really need to be. Normally a new keyword would be a problem in terms of backwards compatibility, but it should be possible to tell from context if the keyword should be treated as such here. It would only be legal to use the keyword if it was immediately followed by a `{` and was the first part of a statement. In all other cases, the keyword would be treated as an identifier. I don't know if this is too complicated in and of itself, but if so there may be alternative syntaxes for this that wouldn't cause such issues. I haven't thought of them, though.

Inside of a `with` block and _only_ inside of a `with` block, very limited pattern matching would be possible. It would use a new assignment operator which I shall assume to be `~=` for illustration purposes. Pattern matching would only work on direct results of functions, not on the internals of values, and would be simple `==` checks.

Here's an example:

```go
with {
  result, nil ~= thingThatCanFail() // This line fails the pattern match if the second return value is not nil.
  val, true ~= result.(T) // Works for all comparable types, not just errors, and even works with type assertions.
  return useValue(val) // Other lines of code are possible, too.
} else {
  case _, error(err):
    return nil, fmt.Errorf("failed to do thing: %w", err)
}
```

The `else` block would be a list of cases looking similar to a `switch` or a `select`. Each case would be a comma-separated list of identifiers, with each being the same as a pattern match above. If any pattern match in the `with` block fails, the same values are run against the cases of the `else` block in top-to-bottom order. If one succeeds, it is run instead and then the whole block exits.

Pattern matches would be very limited in terms of what they could do. Along with being able to check against literals and non-shadowed predefined values, such as `true`, `nil`, etc., they could each be wrapped in what looks like a type conversion. If they are, they are constrained to that specific type. This is demonstrated in the error handling case above. If no cases in the `else` block match, the entire block panics. Or does nothing. I'm not sure which makes more sense.

### Comparison Example

As another example, here's a program that opens two files and copies the contents of one into the other. Here it is without `with`:

```go
src, err := os.Open("input.txt")
if err != nil {
  return err
}
defer src.Close()

dst, err := os.Create("output.txt")
if err != nil {
  return err
}
defer dst.Close()

_, err := io.Copy(dst, src)
if err != nil {
  return err
}
```

And with `with`:

```go
with {
  src, nil ~= os.Open("input.txt")
  defer src.Close()

  dst, nil ~= os.Create("output.txt")
  defer dst.Close()

  _, nil ~= io.Copy(dst, src)
} else {
  case _, error(err):
    return err
}
```

### Primary Concerns

I have three primary issues with my own proposal. I think both of them are solvable, but I'm not quite sure at the moment how to do so.

One is the specific definition for the rules surrounding variable creation. In the above examples, I just kind of assumed that `src` and `dst` were being created by the `~=` operator, but maybe that assumption doesn't make sense. Should it just work exactly like `:=` in terms of variable creation, shadowing, etc? Or should it _never_ create new variables? What are the rules surrounding what is a value on the left-hand side and what is not? Should it only be predeclared values, or should it be possible to match against values stored in variables themselves? Elixir uses the pin operator to declare that a variable should _not_ be declared during a match, i.e. `{^v, r} = something()` meaning that `r` is a new variable but `^v` should just match against the value already stored in `v`. Maybe something like that makes sense? I'm not sure, and I could probably be persuaded either way.

Similarly, I'm worried about ways to differentiate between similar cases that should be handled separately. For example, what if I wanted to add custom error messages to the above example? If all I wanted to add it to was `io.Copy()`, it would be easy enough to just not use pattern matching for that call and handle it the old-fashioned way there. Another option would be to add a third value to indicate, something like

```go
with {
  _, err := io.Copy(dst, src)
  nil, err ~= err, fmt.Errorf("copy failed: %w", err)
} else {
  case _, error(err):
    return err
}
```

And finally, how should overlapping types be handled in `else` cases? For example, if I used the above code in the example before, the `_, error(err)` case would have two different possible types for `_`, `*os.File` and `int64`. Is that a problem? Maybe it could work similarly to #65031. That could get kind of messy, though.

### Conclusion

As you can probably tell from the concerns section, I'm not entirely sold on my own proposal, but I think it's a possibility for a completely different approach to cleaning up error handling code. It doesn't solve every problem people have with error handling, but I think that it leverages multiple returns as part of handling errors more than most proposals, many of which are trying to come up with ways to make functions with multiple returns including an error act as though they are not. That being said, even if this proposal is rejected, maybe it'll inspire a better one.

### Language Spec Changes

Two main changes: Add a `with` block and add the `~=` operator and associated rules.

### Informal Change

_No response_

### Is this change backward compatible?

I think so, but it should be possible to create an alternative that is if it is not. The parts that are potentially no backwards compatible are basically just implementation details.

### Orthogonality: How does this change interact or overlap with existing features?

It enables separation of repeated error handling from the happy path of the code.

### Would this change make Go easier or harder to learn, and why?

Slightly harder, but I don't think that it's overly complicated. It adds a new type of control flow, but I think that the main complication would probably come from the rules surrounding how the pattern matching works.

### Cost Description

Some extra complexity. Possible runtime cost, but very minimal if it exists at all. It's mostly just syntax sugar. Most possible cost could come from some potentially unnecessary type switches, but it might be possible to optimize those away at compile-time in most cases.

### Changes to Go ToolChain

Everything that parses Go code would be affected.

### Performance Costs

Likely minimal in both cases.

### Prototype

_No response_