On Monday, June 5, 2023 at 4:17:17 AM UTC+12 Shulhan wrote:
Dear gophers, I have been reading several proposals about error handling couple of months ago and today a light bulb come upon me, and then I write as much as I can think. I am not sure if its good or bad idea or even possible to implement. In this post, I will try as concise as possible. The full and up to date proposal is available at https://kilabit.info/journal/2023/go2_error_handling/ . Any feedback are welcome so I can see if this can move forward. Thanks in advance. == Background This proposal is based on "go2draft Error Handling". My critics to "go2draft Error Handling" is the missing correlation between handle and check. If we see one of the first code in the design, ---- ... handle err { return fmt.Errorf("copy %s %s: %v", src, dst, err) } r := check os.Open(src) ... ---- There is no explicit link between check keyword and how it will trigger handle err later. It is also break the contract between the signature of os.Open, that return an error in the second parameter, and the code that call it. This proposal try to make the link between them clear and keep the code flow explicit and readable. The goals is not to reduce number of lines but to minimize repetitive error handling. == Proposal This proposal introduces two new keywords and one new syntax for statement. The two new keywords are “WHEN” and “HANDLE”. ---- When = "when" NonZeroValueStmt HandleCallStmt . NonZeroValueStmt = ExpressionStmt ; I am not quite sure how to express non-zero value ; expression here, so I will describe it below. HandleCallStmt = "handle" ( HandleName | "{" SimpleStmt "}" ) . HandleName = identifier . ---- The HandleCallStmt will be executed if only if the statement in NonZeroValueStmt returned non-zero value of its type. For example, given the following variable declarations, ---- var ( err = errors.New(`error`) slice = make([]byte, 1) no1 = 1 no2 int ok bool ) ---- The result of when evaluation are below, ---- when err // true, non-zero value of type error. when len(slice) == 0 // true, non-zero value of type bool. when no1 // true, non-zero value of type int. when no2 // false, zero value of int. when ok // false, zero value of bool. ---- The HandleCallStmt can jump to handle by passing handle name or provide simple statement directly. If its simple statement, there should be no variable shadowing happen inside them. Example of calling handle by name, ---- ... when err handle myErrorHandle :myErrorHandle: return err ---- Example of calling handle using simple statement, ---- ... when err handle { return err } ---- The new syntax for statement is to declare label for handle and its body, ---- HandleStmt = ":" HandleName ":" [SimpleStmt] [ReturnStmt | HandleCallStmt] . ---- Each of `HandleStmt` MUST be declared at the bottom of function block. An `HandleStmt` can call other `HandleStmt` as long as the handle is above the current handle and it is not itself. Any statements below `HandleCallStmt` MUST not be executed. Unlike goto, each `HandleStmt` is independent on each other, one `HandleStmt` end on itself, either by calling `return` or `handle`, or by other `HandleStmt` and does not fallthrough below it. Given the list of handle below, ---- :handle1: S0 S1 :handle2: handle handle1 S3 ---- A `handle1` cannot call `handle2` because its below it. A `handle2` cannot call `handle2`, because its the same handle. A `handle2` can call `handle1`. The `handle1` execution stop at statement `S1`, not fallthrough below it. The `handle2` execution stop at statement "`handle handle1`", any statements below it will not be executed. The following function show an example of using this proposed error handling. Note that the handlers are defined several times here for showing the possible cases on how it can be used, the actual handlers probably only two or three. ---- func ImportToDatabase(db *sql.DB, file string) (error) { when len(file) == 0 handle invalidInput f, err := os.Open(file) when err handle fileOpen // Adding `== nil` is OPTIONAL, the WHEN operation check for NON zero // value of returned function or instance. data, err := parse(f) when err handle parseError err = f.Close() // Inline error handle. when err handle { return fmt.Errorf(`%s: %w`, file, err) } tx, err := db.Begin() when err handle databaseError // One can join the statement with when using ';'. err = doSomething(tx, data); when err handle databaseError err = tx.Commit() when err handle databaseCommitError var outofscope string _ = outofscope // The function body stop here if its not expecting RETURN, otherwise // explicit RETURN must be declared. return nil :invalidInput: // If the function expect RETURN, the compiler will reject and return // an error indicating missing return. :fileOpen: // All the instances of variables declared in function body until this // handler called is visible, similar to goto. return fmt.Errorf(`failed to open %s: %w`, file, err) :parseError: errClose := f.Close() when errClose handle { err = wrapError(err, errClose) } // The value of err instance in this scope become value returned by // wrapError, no shadowing on statement inside inline handle. return fmt.Errorf(`invalid file data: %s: %w`, file, err) :databaseError: _ = db.Rollback() // Accessing variable below the scope of handler will not compilable, // similar to goto. fmt.Println(outofscope) return fmt.Errorf(`database operation failed: %w`, err) :databaseCommitError: // A handle can call another handle as long as its above the current // handle. // Any statements below it will not be executed. handle databaseError RETURN nil // This statement will never be reached. } ---- That's it. What do you guys think? I don't really understand the comparison between this proposal and the referenced previous one. This new proposal effectively makes you have to handle errors for every call site, just like we do now, but with the indirect flow of jumping to a new section of code. And it requires 2 new keywords and new label syntax to achieve it. Could we not replicate this behavior the same way with nested local scope functions as the handlers and just call them with normal if logic? -- You received this message because you are subscribed to the Google Groups "golang-nuts" group. To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts+unsubscr...@googlegroups.com. To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/7e0e9d1a-d60b-4eca-b2c4-eef922a4f4aen%40googlegroups.com.