errors.Join to return a single error if possible

54 views
Skip to first unread message

Andrei Rusakov

unread,
Jul 6, 2024, 10:04:37 AM (2 days ago) Jul 6
to golang-nuts
Hey gophers, I would like to know your opinion on the following topic.

In my experience, the most popular case for errors.Join is to handle deferred potential errors such as closing os.File, sql.Rows or http.Response.Body.

Might be a bit of a contrived example, it get the main point though:
```go
func WriteJSONTo(v any, filename string) (err error) {
  file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0644)
  if err != nil {
    return err
  }

  defer func() { err = errors.Join(err, file.Close()) }()

  return json.NewEncoder(file).Encode(v)
}
```

Although such deferred errors are still worth handling, I assume that they are unlikely to happen. That is, most function calls look like either `errors.Join(nil, nil)` or `errors.Join(someNonNilErr, nil)`

And the function itself always returns either nil or a wrapped trough `*errors.joinError` implementation, even if only one non-nil error is provided.

So, what do you think it makes sense for the function to return a wrapped error even if it contains only one error? Wouldn't it be more rational in such cases for Join to return that single error?

In addition, if such a use case occurs quite often, then avoiding unnecessary allocation could be a pleasant bonus:
```go
func BenchmarkJoinTwoErrors(b *testing.B) {
  for i := 0; i < b.N; i++ {
    _ = Join(io.EOF, io.ErrNoProgress)
  }
}

func BenchmarkJoinOneError(b *testing.B) {
  for i := 0; i < b.N; i++ {
    _ = Join(io.EOF, nil)
  }
}
```

```
goos: linux
goarch: amd64
pkg: fallout
cpu: 12th Gen Intel(R) Core(TM) i9-12900K
                 │    1.txt     │                5.txt                │
                 │    sec/op    │   sec/op     vs base                │
JoinTwoErrors-24    20.87n ± 2%   21.18n ± 2%        ~ (p=0.075 n=10)
JoinOneError-24    17.010n ± 2%   1.321n ± 1%  -92.23% (p=0.000 n=10)
geomean             18.84n        5.290n       -71.92%

                 │   1.txt    │                  5.txt                  │
                 │    B/op    │    B/op     vs base                     │
JoinTwoErrors-24   32.00 ± 0%   32.00 ± 0%         ~ (p=1.000 n=10) ¹
JoinOneError-24    16.00 ± 0%    0.00 ± 0%  -100.00% (p=0.000 n=10)
geomean            22.63                    ?                       ² ³
¹ all samples are equal
² summaries must be >0 to compute geomean
³ ratios must be >0 to compute geomean

                 │   1.txt    │                  5.txt                  │
                 │ allocs/op  │ allocs/op   vs base                     │
JoinTwoErrors-24   1.000 ± 0%   1.000 ± 0%         ~ (p=1.000 n=10) ¹
JoinOneError-24    1.000 ± 0%   0.000 ± 0%  -100.00% (p=0.000 n=10)
geomean            1.000                    ?                       ² ³
¹ all samples are equal
² summaries must be >0 to compute geomean
³ ratios must be >0 to compute geomean
```

Ian Lance Taylor

unread,
Jul 6, 2024, 10:23:36 AM (2 days ago) Jul 6
to Andrei Rusakov, golang-nuts
On Sat, Jul 6, 2024 at 7:04 AM Andrei Rusakov <psih...@gmail.com> wrote:
>
> So, what do you think it makes sense for the function to return a wrapped error even if it contains only one error? Wouldn't it be more rational in such cases for Join to return that single error?

Perhaps that would have made sense, but we can't make a change like
that now that errors.Join has its current behavior.

Ian
Reply all
Reply to author
Forward
0 new messages