{"action":"created","issue":{"url":"https://api.github.com/repos/golang/go/issues/27567","repository_url":"https://api.github.com/repos/golang/go","labels_url":"https://api.github.com/repos/golang/go/issues/27567/labels{/name}","comments_url":"https://api.github.com/repos/golang/go/issues/27567/comments","events_url":"https://api.github.com/repos/golang/go/issues/27567/events","html_url":"https://github.com/golang/go/issues/27567","id":358300532,"node_id":"MDU6SXNzdWUzNTgzMDA1MzI=","number":27567,"title":"proposal: Go 2: error handling with functions and an error return?","user":{"login":"gregwebs","id":1183,"node_id":"MDQ6VXNlcjExODM=","avatar_url":"https://avatars2.githubusercontent.com/u/1183?v=4","gravatar_id":"","url":"https://api.github.com/users/gregwebs","html_url":"https://github.com/gregwebs","followers_url":"https://api.github.com/users/gregwebs/followers","following_url":"https://api.github.com/users/gregwebs/following{/other_user}","gists_url":"https://api.github.com/users/gregwebs/gists{/gist_id}","starred_url":"https://api.github.com/users/gregwebs/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/gregwebs/subscriptions","organizations_url":"https://api.github.com/users/gregwebs/orgs","repos_url":"https://api.github.com/users/gregwebs/repos","events_url":"https://api.github.com/users/gregwebs/events{/privacy}","received_events_url":"https://api.github.com/users/gregwebs/received_events","type":"User","site_admin":false},"labels":[{"id":236419512,"node_id":"MDU6TGFiZWwyMzY0MTk1MTI=","url":"https://api.github.com/repos/golang/go/labels/Proposal","name":"Proposal","color":"ededed","default":false}],"state":"closed","locked":true,"assignee":null,"assignees":[],"milestone":{"url":"https://api.github.com/repos/golang/go/milestones/30","html_url":"https://github.com/golang/go/milestone/30","labels_url":"https://api.github.com/repos/golang/go/milestones/30/labels","id":1373555,"node_id":"MDk6TWlsZXN0b25lMTM3MzU1NQ==","number":30,"title":"Proposal","description":"Proposals that are pending (not yet accepted/rejected).\r\n","creator":{"login":"rsc","id":104030,"node_id":"MDQ6VXNlcjEwNDAzMA==","avatar_url":"https://avatars1.githubusercontent.com/u/104030?v=4","gravatar_id":"","url":"https://api.github.com/users/rsc","html_url":"https://github.com/rsc","followers_url":"https://api.github.com/users/rsc/followers","following_url":"https://api.github.com/users/rsc/following{/other_user}","gists_url":"https://api.github.com/users/rsc/gists{/gist_id}","starred_url":"https://api.github.com/users/rsc/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/rsc/subscriptions","organizations_url":"https://api.github.com/users/rsc/orgs","repos_url":"https://api.github.com/users/rsc/repos","events_url":"https://api.github.com/users/rsc/events{/privacy}","received_events_url":"https://api.github.com/users/rsc/received_events","type":"User","site_admin":false},"open_issues":238,"closed_issues":593,"state":"open","created_at":"2015-10-24T01:40:49Z","updated_at":"2018-09-19T17:42:18Z","due_on":null,"closed_at":null},"comments":53,"created_at":"2018-09-08T14:38:48Z","updated_at":"2018-09-19T17:42:18Z","closed_at":"2018-09-19T17:42:18Z","author_association":"NONE","body":"# Background (go 2 Process)\r\n\r\ngo 2 has laid out the [problem of error handling](https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md) (Please read before reading this proposal).\r\n\r\nI am told that alternative proposals should be raised as git issues here. Please add anyone else you think would like to join the discussion.\r\n\r\n\r\n# Introduction\r\n\r\nIt is amazing to see the [error handling problem](https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md) being properly addressed. The [existing draft proposal](https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling.md) is good, but I think we can iterate to make it better.\r\nTo avoid confusion by comparison to the existing proposal, I will avoid mentioning it in this one.\r\nHowever, if you are a supporter of the existing proposal, please separately read my [critique](https://gist.github.com/gregwebs/cdaaaae475ae3012730694598a24e0b9) of it.\r\n\r\nIt's useful to take a step back and clearly state what we are trying to do with our implementation:\r\n\r\n* provide an abstraction that allows for the insertion of a return statement for errors.\r\n* compose handler functions together before they are used with the error return\r\n\r\nIn the existing go language, I cannot write a handler function which will create an early return from the function.\r\nThere are a few approaches that use existing languages features for this control flow:\r\n* Macros (e.g. Rust originally used a `try!` macro).\r\n* Ruby supports anonymous functions that return from the enclosing function (method)\r\n* exceptions\r\n* Sequencing code with short-circuits. Some usage of monads in Haskell are a particularly good example of this.\r\n\r\nFor sequences with short-circuits, see the errors are values post for how this can be done in go. However, this severely alters how one would write go code.\r\n\r\n\r\n# Proposal: handlers as functions, just a special check\r\n\r\nLets repeat our goals again:\r\n\r\n* provide an abstraction that allows for the insertion of a return statement for errors.\r\n* compose handler functions together before they are used with the error return\r\n\r\nComposition can be handled with ordinary functions that take and return an error.\r\n\r\nThat means we just need a mechanism to insert a `return`.\r\nFor early return in my proposal, I will use a question mark operator `?` rather than a `check` keyword. This is for two reasons\r\n* the operator can be used postfix, which has readability advantages\r\n* the original draft proposal used `check`, but it functions differently, so this may help avoid confusion.\r\n\r\nSee \"Appendix: Operator versus check function\" for a discussion on using `?` or a `check` keyword. \r\n\r\n\r\n## Implementation as syntactic expansion\r\n\r\n``` go\r\nv := f() ? handler\r\n```\r\nexpands to\r\n\r\n``` go\r\nv, err := f()\r\nif err != nil {\r\n    return Zero, handler(err)\r\n}\r\n```\r\n\r\nWhere `handler` is a function that takes an `error` and returns one. `Zero` is the zero value for the (success) value returned before the error, assuming the function returns a single success value. A function that returns 4 values, the last one being an error, would have.\r\n\r\n``` go\r\n    return Zero, Zero, Zero, handler(err)\r\n```\r\n\r\nThis is a simple, easy to understand transformation. It is easy to underestimate the value from being able to [understand the usage site without searching for context](https://github.com/golang/go/issues/27567#issuecomment-421204131). I am trying to avoid comparisons to other proposals, but I want to say that none of the others I have seen can be described this simply.\r\n\r\nAll of the transformation is performed entirely by `?`. It inserts the nil check, the `return`, and creates the needed zero values. The handler is just a normal function and an argument to `?`.\r\n\r\nFor some small convenience in writing cleanup handlers, the return section would actually expand to this:\r\n\r\n``` go\r\n    return Zero, handler.ToModifyError()(err)\r\n```\r\n\r\nSee the section on handler types and the appendix section on `ThenErr` and `ToModifyError`.\r\n\r\n\r\n## Basic example from the original proposal, re-written\r\n\r\nPutting this together, lets re-write SortContents, which wants different handlers in different places.\r\n\r\n``` go\r\nfunc SortContents(w io.Writer, files []string) error {\r\n    handlerAny := func(err error) error {\r\n\treturn fmt.Errorf(\"process: %v\", err)\r\n    }\r\n\r\n    lines := []strings{}\r\n    for _, file := range files {\r\n\thandlerFiles := func(err error) error {\r\n\t    return fmt.Errorf(\"read %s: %v \", file, err)\r\n\t}\r\n\tscan := bufio.NewScanner(os.Open(file) ? handlerFiles)\r\n\tfor scan.Scan() {\r\n\t    lines = append(lines, scan.Text())\r\n\t}\r\n\tscan.Err() ? handlerFiles\r\n    }\r\n    sort.Strings(lines)\r\n    for _, line := range lines {\r\n\tio.WriteString(w, line) ? handlerAny\r\n    }\r\n}\r\n```\r\n\r\nLet's show another example from the proposal (slightly simplified) that has handler composition:\r\n\r\n``` go\r\nfunc process(user string, files chan string) (n int, err error) {\r\n    ahandler := func(err error) error { return fmt.Errorf(\"process: %v\", err) }\r\n    for i := 0; i < 3; i++ {\r\n\tbhandler := func(err error) error { return fmt.Errorf(\"attempt %d: %v\", i, err) }\r\n\tdo(something()) ? ahandler.ThenErr(bhandler)\r\n    }\r\n    do(somethingElse()) ? ahandler\r\n}\r\n```\r\n\r\nIt is possible to combine handlers in the same way one would combine functions:\r\n\r\n``` go\r\ndo(something()) ? ahandler.ThenErr(func(err error) error {\r\n\treturn fmt.Errorf(\"attempt %d: %v\", i, err) }\r\n)\r\n```\r\n\r\nOr\r\n\r\n``` go\r\ndo(something()) ? func(err error) { return ahandler(bhandler(err)) }\r\n```\r\n\r\nThe example uses a `.ThenErr` method (see appendix) as a way to compose error handler functions together. \r\n\r\n\r\n## Results\r\n\r\n* This alternative proposal introduces just one special construct, `?`\r\n* The programmer has control and flexibility in the error handling.\r\n* Handlers can be naturally composed as functions\r\n* The code is much more succinct and organized than current go error handling code.\r\n* errors can be returned from `defer`.\r\n\r\n\r\n## Checking error returns from deferred calls\r\n\r\nThis alternative proposal can support returning errors from `defer`:\r\n\r\n\tdefer w.Close() ? closeHandler\r\n\r\n\r\n\r\n## Notes on error handler function types\r\n\r\nTo respond to errors we want to do one of two things:\r\n* cleanup (side-effecting): `(error) -> nil` or `() -> nil`\r\n* modify the error: `(error) -> error`\r\n\r\nAn error handler function must always have the type of the modifier, but we may not want the extra noise when writing a purely cleanup handler. The question mark operator can accept all forms. A cleanup function can be automatically converted to return the original error that would have been passed to it.\r\n\r\nThis is also true of helpers that compose error functions such as `ThenErr`.\r\nSee the Appendix section on `ThenErr` to see how this is implemented.\r\n\r\n\r\n# Appendix\r\n\r\n## Appendix: Handle and anonymous function syntax\r\n\r\nThis proposal is slightly more verbose than others that introduce a special anonymous function syntax that is lighter-weight and infers types.\r\n\r\n``` go\r\nhandle err { return fmt.Errorf(\"process: %v\", err) }\r\n```\r\n\r\nWithout this syntax, the proposal would read:\r\n\r\n``` go\r\nhandle func(err error) error { return fmt.Errorf(\"process: %v\", err) }\r\n```\r\n\r\nI think it is worthwhile to explore having anonymous functions that are lighter-weight.\r\nHowever, I think this should be usable anywhere rather than just with a single keyword.\r\n\r\nBut please leave this for another proposal rather than throw it in the mix with error handlers!\r\n\r\n\r\n## Appendix: unary and binary.\r\n\r\nThe question mark operator can be used as a unary to just return the exception without any handlers running.\r\n\r\n``` go\r\nsomething()?\r\n```\r\n\r\nThis is equivalent to \r\n\r\n``` go\r\nsomething() ? func(err error) error { return err }\r\n```\r\n\r\nI am favoring writing the unary form without any spaces in this case (more similar to Rust), but we should use whatever syntax the community finds best.\r\n\r\n\r\n## Appendix: Handling errors within the handler itself\r\n\r\nA cleanup handler may generate a new error that should be propagated in addition to the current error.\r\nI believe this should just be handled by a multi-error technique, e.g. [multierr](https://github.com/uber-go/multierr/blob/master/error.go).\r\n\r\n\r\n## Appendix: custom error types\r\n\r\nThe existing proposal seems like it would cast a concrete error type to the `error` interface when it is passed to a handler.\r\nI don't think this proposal is fundamentally different.\r\nI think this issue should be solved by the generics proposal.\r\n\r\n\r\n## Appendix: ThenErr and ToModifyErr\r\n\r\nAn implementation of ThenErr  and ToModifyErr. See the syntax expansion section for how the `?` operator uses `ToModifyError`.\r\n\r\n``` go\r\ntype Cleanup func(error)\r\ntype CleanupNoError func()\r\ntype ModifyError func(error) error\r\n\r\ntype ToModifyError interface {\r\n\tToModifyError() ModifyError\r\n}\r\n\r\nfunc (fn1 ModifyError) ToModifyError() ModifyError {\r\n\treturn fn1\r\n}\r\n\r\nfunc (fn1 CleanupNoError) ToModifyError() ModifyError {\r\n\treturn func(err error) error {\r\n\t\tfn1()\r\n\t\treturn err\r\n\t}\r\n}\r\n\r\nfunc (fn1 Cleanup) ToModifyError() ModifyError {\r\n\treturn func(err error) error {\r\n\t\tfn1(err)\r\n\t\treturn err\r\n\t}\r\n}\r\n\r\n// Its easier to write this once as a function\r\nfunc CombineErrs(funcs ...ToModifyError) ModifyError {\r\n\treturn func(err error) error {\r\n\t\tfor _, fn := range funcs {\r\n\t\t\terr = fn.ToModifyError()(err)\r\n\t\t}\r\n\t\treturn err\r\n\t}\r\n}\r\n\r\n// But method syntax is convenient\r\ntype ErrorHandlerChain interface {\r\n\tThenErr(ToModifyError) ModifyError\r\n}\r\n\r\nfunc (fn1 ModifyError) ThenErr(fn2 ToModifyError) ModifyError {\r\n\treturn func(err error) error {\r\n\t\treturn fn1(fn2.ToModifyError()(err))\r\n\t}\r\n}\r\n\r\nfunc (fn1 Cleanup) ThenErr(fn2 ToModifyError) ModifyError {\r\n\treturn func(err error) error {\r\n\t\tfn1(err)\r\n\t\treturn fn2.ToModifyError()(err)\r\n\t}\r\n}\r\n\r\nfunc (fn1 CleanupNoError) ThenErr(fn2 ToModifyError) ModifyError {\r\n\treturn func(err error) error {\r\n\t\tfn1()\r\n\t\treturn fn2.ToModifyError()(err)\r\n\t}\r\n}\r\n```\r\n\r\n\r\n## Appendix: Operator versus check function\r\n\r\nThe original proposal [rejected the question mark](https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling.md#considered-ideas) and gave some reasons why.\r\nSome of those points are still valid with this proposal, and others are not.\r\n\r\nHere is [another proposal](https://gist.github.com/PeterRK/4f59579c1162cdbc28086f6b5f7b4fa2) that I believe advocates the same solution proposed in this alternative, but with a `check` function. I would be happy with that as a solution, but below I give my preference for `?`.\r\n\r\nThe original proposal had just one argument given to `check`. This alternative favors the question mark in large part because there are now 2 arguments.\r\nThe original proposal states that there is a large readability difference in these two variants:\r\n\r\n``` go\r\ncheck io.Copy(w, check newReader(foo))\r\nio.Copy(w, newReader(foo)?)?\r\n```\r\n\r\nHowever, I think this is a matter of personal preference. Once there is a left-hand-side assignment, the readability opinion may also change.\r\n\r\n``` go\r\ncopied := check io.Copy(w, check newReader(foo))\r\ncopied := io.Copy(w, newReader(foo)?)?\r\n```\r\n\r\nNow lets add in a handlers and check our preference again.\r\n\r\n``` go\r\ncopied := check(io.Copy(w, check(newReader(foo), ahandler), bhandler)\r\ncopied := io.Copy(w, newReader(foo) ? ahandler) ? bhandler\r\n```\r\n\r\nI believe `?` will be slightly nicer to use due to\r\n* fewer parantheses\r\n* putting error handling solely on the right-hand-side rather than both the left and right.\r\n\r\nNote that it is also possible to put all the error handling on the left-hand-side of the error source.\r\n\r\n``` go\r\ncopied := check(bhandler, io.Copy(w, check(ahandler, newReader(foo)))\r\n```\r\n\r\nBut I prefer keeping error handling on the right-hand-side for two reasons\r\n* a success result is still transferred to the left\r\n* it is possible to write an anonymous handler rather than being forced to declare it ahead of time\r\n\r\n\r\n## Appendix: built-in result type\r\n\r\nA go programmer that has used Rust, Swift, Haskell, etc will be missing a real result type.\r\nI would like to see a go 2 proposal for discriminated unions which includes a result type.\r\nHowever, I think both the original proposal and this alternative proposal would work fine with the addition of a result type.\r\nThis is because go effectively already has a result type when dealing with errors. It is a tuple where the last member is of type `error`.\r\n\r\nA future version of go with discriminated unions should be able to use `?` for dealing with a discriminated union result type.\r\n\r\n\r\n## Appendix: intermediate bindings for readability\r\n\r\nError handling on the right-hand-side may increase line length undesirably or seem to be easy to miss. Its always possible to use an intermediate binding.\r\n\r\n```\r\nv, err := f(...) // could be a million lines long\r\nerr ? handler\r\n```\r\n\r\n## Appendix: left-hand-side\r\n\r\nIt is possible to support placing the handler on the left-hand-side.\r\n\r\n```\r\nv := handler ? f(...)\r\n```\r\n\r\nThis could make more sense for `check`. One of the ideas behind this would be to emphasize the handler, for example in the case where `f(...)` is an enormous expression (see above section on intermediate bindings which is another way to handle this).\r\n\r\n## Appendix: returning the zero value\r\n\r\nThis proposal does not allow for the defensive practice of returning `-1` as the success value, along with the error. Where `-1` is useful because zero or a positive number are an allowed value in the problem domain, so someone may notice a `-1` propagating. I don't think we need to support this use case for a few reasons:\r\n\r\n  * It is not generally applicable anyways (consider a `uint`).\r\n  * The contract of using the function is already that errors must be checked before looking at success values.\r\n  * There are standard linters (errcheck) that will warn people about ignoring errors: we should instead ship this ability with `go vet`.\r\n\r\n\r\n## Appendix: all proposal examples re-written\r\n\r\nBelow are the rest of the code snippets shown in the original proposal, transformed to this alternative proposal.\r\n\r\n``` go\r\nfunc TestFoo(t *testing.T) {\r\n\thandlerFatal := func(err error) { t.Fatal(err) }\r\n\tfor _, tc := range testCases {\r\n\t\tx := Foo(tc.a) ? handlerFatal\r\n\t\ty := Foo(tc.b) ? handlerFatal\r\n\t\tif x != y {\r\n\t\t\tt.Errorf(\"Foo(%v) != Foo(%v)\", tc.a, tc.b)\r\n\t\t}\r\n\t}\r\n}\r\n\r\nfunc printSum(a, b string) error {\r\n\thandler := func(err error) error { fmt.Errorf(\"printSum(%q + %q): %v\", a, b, err) }\r\n\tx := strconv.Atoi(a) ? handler\r\n\ty := strconv.Atoi(b) ? handler\r\n\tfmt.Println(\"result:\", x + y)\r\n\treturn nil\r\n}\r\n\r\nfunc printSum(a, b string) error {\r\n\tfmt.Println(\"result:\", strconv.Atoi(x)? + strconv.Atoi(y)?)\r\n\treturn nil\r\n}\r\n\r\nfunc CopyFile(src, dst string) error {\r\n\thandlerBase := func(err error) error {\r\n\t\treturn fmt.Errorf(\"copy %s %s: %v\", src, dst, err)\r\n\t}\r\n\r\n\tr := os.Open(src) ? handlerBase\r\n\tdefer r.Close()\r\n\r\n\tw := os.Create(dst) ? handlerbase\r\n\thandlerWithCleanup := handlerBase.ThenErr(func(err error) {\r\n\t\tw.Close()\r\n\t\tos.Remove(dst) // (only if a check fails)\r\n\t})\r\n\r\n\tcheck io.Copy(w, r) ? handlerWithCleanup\r\n\tcheck w.Close() ? handlerWithCleanup\r\n\treturn nil\r\n}\r\n\r\n\r\nfunc main() {\r\n\thandlerAll := func(err error) error {\r\n\t\tlog.Fatal(err)\r\n\t}\r\n\r\n\thex := check ioutil.ReadAll(os.Stdin) ? handlerAll\r\n\tdata := check parseHexdump(string(hex)) ? handlerAll\r\n\tos.Stdout.Write(data)\r\n}\r\n```"},"comment":{"url":"https://api.github.com/repos/golang/go/issues/comments/422894596","html_url":"https://github.com/golang/go/issues/27567#issuecomment-422894596","issue_url":"https://api.github.com/repos/golang/go/issues/27567","id":422894596,"node_id":"MDEyOklzc3VlQ29tbWVudDQyMjg5NDU5Ng==","user":{"login":"rsc","id":104030,"node_id":"MDQ6VXNlcjEwNDAzMA==","avatar_url":"https://avatars1.githubusercontent.com/u/104030?v=4","gravatar_id":"","url":"https://api.github.com/users/rsc","html_url":"https://github.com/rsc","followers_url":"https://api.github.com/users/rsc/followers","following_url":"https://api.github.com/users/rsc/following{/other_user}","gists_url":"https://api.github.com/users/rsc/gists{/gist_id}","starred_url":"https://api.github.com/users/rsc/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/rsc/subscriptions","organizations_url":"https://api.github.com/users/rsc/orgs","repos_url":"https://api.github.com/users/rsc/repos","events_url":"https://api.github.com/users/rsc/events{/privacy}","received_events_url":"https://api.github.com/users/rsc/received_events","type":"User","site_admin":false},"created_at":"2018-09-19T17:42:18Z","updated_at":"2018-09-19T17:42:18Z","author_association":"CONTRIBUTOR","body":"The right way to submit feedback and alternate designs like this is to post it somewhere else (a blog post, Medium, a Gist, etc) and then link it from the wiki page. See https://go.googlesource.com/proposal/+/master/design/go2draft.md. Thanks.\r\n\r\n"}}
{"actor":{"display_login":"rsc"}}