Skip to content

Commit 7c3b687

Browse files
committed
README: fix syntax issues and improve language
1 parent 2141675 commit 7c3b687

File tree

1 file changed

+136
-97
lines changed

1 file changed

+136
-97
lines changed

Diff for: README.md

+136-97
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,41 @@
11
# Error handling proposal
22

3-
**This is not a formal go proposal yet, but the pre-work needed in order to potentially create one.**
3+
**This should not be considered a formal Go proposal yet, but the pre-work needed in order to create one. When all requirements are in place, this may transition into a proposal.**
4+
5+
Contribution guide:
6+
7+
- Discuss changes first; either, either on the [Go issue][issue], or by raising an issue at the [GitHub repo][repo].
8+
- Commits should be atomic; rebase your commits to match this.
9+
- Examples with third-party dependencies should get it's own go.mod file.
10+
- Include both working Go 1.24 code (with .go suffix), and a variant using the proposed ? syntax (with .go2 suffix). -
11+
- Note that only files that are affected by the proposal syntax, needs a .go2 file.
12+
13+
Contributions that may be of particular interest for now:
14+
15+
- Contributions demonstrating how this change would help improve application code.
16+
- Pointing out potential issues.
17+
18+
Links:
19+
20+
- Pre-work repository: [smyrman/error-handling-proposal][repo]
21+
- Go issue: [#72149][issue]
22+
23+
[issue]: https://github.com/golang/go/issues/72149
24+
[repo]: https://github.com/smyrman/error-handling-proposal
25+
26+
Remaining content is aligned with the issue text.
27+
428
### Go Programming Experience
529

6-
Experienced
30+
(Contributors may add their own replies)
31+
32+
@smyrman: Experienced
733

834
### Other Languages Experience
935

10-
Python, C
36+
(Contributors may add their own replies)
37+
38+
@smyrman: C++, C, Python
1139

1240
### Related Idea
1341

@@ -38,7 +66,6 @@ Semantically, this proposal is somewhat similar to [try-catch][try-catch] propos
3866
[discussion]: https://github.com/golang/go/discussions/71460#discussioncomment-12365387
3967
[try-catch]: https://github.com/golang/go/issues/32437
4068

41-
4269
### Does this affect error handling?
4370

4471
Yes
@@ -53,6 +80,8 @@ It's not about generics, but the proto-type is using generics for it's implement
5380

5481
### Cases
5582

83+
Before discussing the proposal, we will demonstrate a few use-cases that could benefit from it. The cases will be relatively simple. Real use-cases may be more complex, and could therefore expect to result in saving more lines.
84+
5685
#### Return directly
5786

5887
The direct return of an error is a commonly used case for error handling when adding additional context is not necessary.
@@ -61,15 +90,14 @@ Old syntax:
6190
```go
6291
pipeline, err := A()
6392
if err != nil {
64-
return err
93+
return err
6594
}
6695
pipeline, err = pipeline.B()
6796
if err != nil {
68-
return err
97+
return err
6998
}
7099
```
71100
New syntax:
72-
73101
```go
74102
pipeline := A()?.B()?
75103
```
@@ -82,60 +110,60 @@ Old syntax:
82110
```go
83111
pipeline, err := A()
84112
if err != nil {
85-
return fmt.Errorf("a: %w", err)
113+
return fmt.Errorf("a: %w", err)
86114
}
87115
pipeline = pipeline.B()
88116
if err != nil {
89-
return fmt.Errorf("a: %w (pipeline ID: %s)", err, id)
117+
return fmt.Errorf("a: %w (pipeline ID: %s)", err, id)
90118
}
91119
```
92120

93121
New Syntax:
94122
```go
95123
pipeline :=
96-
A() ?(errors.Wrap("a: %w")).
97-
B() ?(errors.Wrap("b: %[2]w (pipeline ID: %[1]s)", id))
124+
A() ?(errors.Wrap("a: %w")).
125+
B() ?(errors.Wrap("b: %[2]w (pipeline ID: %[1]s)", id))
98126
```
99127

100128
#### Collect errors
101129

102-
The collect errors case appear less common in Go for a few reasons. First of all, it's hard to do it as the standard mechanisms for handling it is limited. Secondly, most open source Go code is libraries. However, the use-case for collecting errors is likely common in application code. Especially code that relates to some sort of UI form-validation of user input. Another related example is an API that want to validate client input and communicate all errors at once so that the API client maintainers can more easily do their job.
130+
The case for collecting errors is likely not common in library code. However, it is likely useful for application code. Possible use-cases include form validation or JSON APIs.
103131

104132
Old syntax:
105133
```go
106134
func ParseMyStruct(in transportModel) (BusinessModel, error) {
107-
var errs []error
108-
a, err := ParseA(in.A)
109-
if err != nil {
110-
errs = append(fmt.Errorf("a: %w", err))
111-
}
112-
b, err := ParseB(in.A)
113-
if err != nil {
114-
errs = append(fmt.Errorf("b: %w", err))
115-
}
116-
if err := errors.Join(errs...); err != nil {
117-
return BusinessModel{}, err
118-
}
135+
var errs []error
136+
a, err := ParseA(in.A)
137+
if err != nil {
138+
errs = append(fmt.Errorf("a: %w", err))
139+
}
140+
b, err := ParseB(in.A)
141+
if err != nil {
142+
errs = append(fmt.Errorf("b: %w", err))
143+
}
144+
if err := errors.Join(errs...); err != nil {
145+
return BusinessModel{}, err
146+
}
119147

120-
return BusinessModel{
121-
A: a,
122-
B: b,
123-
}, nil
148+
return BusinessModel{
149+
A: a,
150+
B: b,
151+
}, nil
124152
}
125153
```
126154

127155
New Syntax:
128156
```go
129157
func ParseMyStruct(in transportModel) (BusinessModel, error) {
130-
var c errors.Collector
131-
out := BusinessModel{
132-
A: ParseA(in.A) ?(errors.Wrap("a: %w"), c.Collect),
133-
B: ParseB(in.B) ?(errors.Wrap("b: %w"), c.Collect),
134-
}
135-
if err := c.Err(); err != nil {
136-
return BusinessModel{}, err
137-
}
138-
return out, nil
158+
var c errors.Collector
159+
out := BusinessModel{
160+
A: ParseA(in.A) ?(errors.Wrap("a: %w"), c.Collect),
161+
B: ParseB(in.B) ?(errors.Wrap("b: %w"), c.Collect),
162+
}
163+
if err := c.Err(); err != nil {
164+
return BusinessModel{}, err
165+
}
166+
return out, nil
139167
}
140168
```
141169

@@ -156,58 +184,61 @@ func (err PathError) Error() string {
156184
Old syntax:
157185
```go
158186
func ParseMyStruct(in transportModel) (BusinessModel, error) {
159-
var errs []error
160-
a, err := ParseA(in.A)
161-
if err != nil {
162-
errs = append(PathError{Path:"a", Err: err))
163-
}
164-
b, err := ParseB(in.A)
165-
if err != nil {
166-
errs = append(PathError{Path:"b", Err: err))
167-
}
168-
if err := errors.Join(errs...); err != nil {
169-
return BusinessModel{}, err
170-
}
187+
var errs []error
188+
a, err := ParseA(in.A)
189+
if err != nil {
190+
errs = append(PathError{Path:"a", Err: err))
191+
}
192+
b, err := ParseB(in.A)
193+
if err != nil {
194+
errs = append(PathError{Path:"b", Err: err))
195+
}
196+
if err := errors.Join(errs...); err != nil {
197+
return BusinessModel{}, err
198+
}
171199

172-
return BusinessModel{
173-
A: a,
174-
B: b,
175-
}, nil
200+
return BusinessModel{
201+
A: a,
202+
B: b,
203+
}, nil
176204
}
177205
```
178206

179207
New Syntax (inline handler):
180208
```go
181209
func ParseMyStruct(in transportModel) (BusinessModel, error) {
182-
var errs []error
183-
out := BusinessModel{
184-
A: ParseA(in.A) ?(func(err error) error{
185-
errs = append(PathError{Path:"a", Err: err))
186-
},
187-
B: ParseB(in.B) ?(func(err error) error{
188-
errs = append(PathError{Path:"b", Err: err))
189-
},
210+
var errs []error
211+
out := BusinessModel{
212+
A: ParseA(in.A) ?(func(err error) error{
213+
errs = append(PathError{Path:"a", Err: err))
214+
}),
215+
B: ParseB(in.B) ?(func(err error) error{
216+
errs = append(PathError{Path:"b", Err: err))
217+
}),
190218
}
191219
if err := errors.Join(errs...) {
192-
return BusinessModel{}, err
193-
}
194-
return out, nil
195-
}
196-
```
220+
return BusinessModel{}, err
221+
}
222+
return out, nil
223+
}
224+
```
197225

198226
### Proposal
199227

200-
**This is not a complete proposal yet. Instead, it's in an investigative phase, and I am creating this issue to identify the next steps.**
201-
202-
Pre-work and initial design can be found here:
203-
- https://github.com/smyrman/error-handling-proposal
204-
205228
The proposal has two parts:
206-
- An addition to the Go syntax.
229+
- An addition to the Go syntax, using `?()` /`?` to catch errors.
207230
- Helper functions in the `errors` package.
208231

209232
The proposal follows the principal of the now implemented range-over-func proposal in making sure that the solution can be described as valid Go code using the current language syntax. As of the time of writing, this is the syntax of Go 1.24.
210233

234+
The `?`/`?()` syntax can be used to _move handling_ of errors from the left of the expression to the right. The default handler (no parenthesis), is to return on error. When parenthesis are provided, errors pass though handlers of format `func(error) error`. If any handler return nill, the code continuous along the happy path. If the final handler returns an error, the function with the `?` syntax returns.
235+
236+
It's not yet clear if the `?` syntax should be allowed inside functions that does _not_ return an error. If it's allowed, the suggestion is that the `?` syntax would result in a panic. See options for more details.
237+
238+
The standard library changes involve adding handlers for the most common cases for error handling.
239+
240+
### Standard library changes
241+
211242
The following exposed additions to the standard library `errors` package is suggested:
212243

213244
```go
@@ -282,23 +313,24 @@ If the `?` operator receives an error, the error is passed to each handler in or
282313

283314
If after all handlers are called, the final return value is an error, then the flow of the current _statement_ is aborted similar to how a panic works. If `?` is used within a function where the final return statement is an error, then this panic is _recovered_ and the error value is populated with that error value and the function returns at once.
284315

316+
285317
### Is this change backward compatible?
286318

287319
Yes
288320

289-
This work leans on the work done for [%71460][https://github.com/golang/go/discussions/71460], that highlights that the `?` operator is invalid to use in any existing code. Thus it's expected that no existing code will be able to break due to the introduction of the new syntax.
321+
This work leans on the work done for [%71460][discussion], that highlights that the `?` operator is invalid to use in any existing code. Thus it's expected that no existing code will be able to break due to the introduction of the new syntax.
290322

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

293325
_No response_
294326

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

297-
Any addition to the Go syntax, including this one, will make it harder to learn go, including this one.
329+
Any addition to the Go syntax, including this one, will make it harder to learn Go. However, people coming from an exception handling paradigm may find the new syntax less intrusive then the explicit return.
298330

299331
### Cost Description
300332

301-
The highest cost of this proposal is that there will now be multiple patterns for handling errors.
333+
The highest cost of this proposal is likely that there will now be multiple patterns for handling errors. There could be discrepancies and disagreement between different projects about which style to use.
302334

303335
### Changes to Go ToolChain
304336

@@ -310,35 +342,32 @@ _No response_
310342

311343
### Prototype
312344

313-
https://github.com/smyrman/error-handling-proposal includes a working proto-type for the program _flow_ that the new syntax suggests. However there is no transpiler for converting the proposed syntax into valid Go code. In theory one could be written, but I do not have the required time or skills to do so.
314-
315-
316-
## Implementation without a language change
345+
The proto-type code is found in the [pre-work repo][repo].
317346

318-
Following the example of range-over-func, the implementation of the `?` semantics is not magic. A tool can be written to generate go code that rewrites the `?` syntax to valid go 1.24 syntax.
347+
Following the example of range-over-func, the implementation of the `?` semantics is not magic. A tool could be written to generate go code that rewrites the `?` syntax to valid go 1.24 syntax.
319348

320349
With proposed syntax:
321350
```go
322351
func AB() (Pipeline, error) {
323-
id := "test"
324-
result :=
325-
A() ?(errors.Wrap("a: %w")).
326-
B() ?(errors.Wrap("b: %[2]w (pipeline ID: %[1]s)", id))
327-
return result, nil
352+
id := "test"
353+
result :=
354+
A() ?(errors.Wrap("a: %w")).
355+
B() ?(errors.Wrap("b: %[2]w (pipeline ID: %[1]s)", id))
356+
return result, nil
328357
}
329358
````
330359

331360
Can be written using the proto-type library as:
332361

333362
```go
334363
func AB() (_ Pipeline, _err error) {
335-
defer errors.Catch(&_err) // Added to the top of all function bodies that contain a `?` operator.
364+
defer errors.Catch(&_err) // Added to the top of all function bodies that contain a `?` operator.
336365
337-
id := "test"
338-
result :=
339-
xerrors.Must2(A())(xerrors.Wrap("a: %w")). // function syntax for ?
340-
xerrors.Must2(B())(xerrors.Wrap("b: %[2]w (pipeline ID: %[1]s)", id)) // function syntax for ?
341-
return result, nil
366+
id := "test"
367+
result :=
368+
xerrors.Must2(A())(xerrors.Wrap("a: %w")). // function syntax for ?
369+
xerrors.Must2(B())(xerrors.Wrap("b: %[2]w (pipeline ID: %[1]s)", id)) // function syntax for ?
370+
return result, nil
342371
}
343372
```
344373

@@ -440,23 +469,33 @@ func Must2[T any](v T, err error) func(handlers ...func(error) error) T {
440469
}
441470
```
442471

443-
## Options
472+
### Options
444473

445-
### Disallow usage within non-error functions
474+
Options could be applied to change the proposal in various ways.
475+
476+
#### 1: Disallow usage within non-error functions
446477

447478
We could choose to disallow the `?` syntax inside functions that doesn't return errors. This included the `main` function.
448479
449-
### Allow explicit catch
480+
This would ensure that the `?` syntax _can not_ lead to panics.
481+
482+
#### 2: Allow explicit catch
450483
451484
An option could be to expose the `Catch` function from the proto-type, and allow supplying a set of error handlers that run on all errors.
452485
453486
When an explicit Catch is added, then an implicit Catch is not added.
454487
455488
If the Catch is called with a nil pointer, then any error that isn't fully handled (replaced by `nil`), results in a panic.
456489

457-
## Why not...
490+
#### 3: Chain via ? syntax
491+
492+
Use syntax `? handler1 ? handler2` as shown in [this comment][comment-by-733amir] by @733amir.
493+
494+
[comment-by-733amir]: https://github.com/golang/go/discussions/71460#discussioncomment-12418576
495+
496+
### Why not...
458497

459-
### Why not allow more than two return values?
498+
#### Why not allow more than two return values?
460499

461500
```go
462501
a, b, err := A()
@@ -471,7 +510,7 @@ Most functions that return an error, return either a single parameter, or two pa
471510
472511
Allowing for more return values risks complicating the implementation, and is likely offer little value in return.
473512
474-
### Why require the final return parameter to be an error?
513+
#### Why require the final return parameter to be an error?
475514
476515
```go
477516
a := os.Getenv("VARIABLE")? // not allowed

0 commit comments

Comments
 (0)