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?