errors...
Opened this issue · 10 comments
Imagine the following syntax for errors
// this function will return an error or int32 (zig handles errors this way)
// I think zig uses a union?
func add(a, b int32) (int32, error) {
return errors.New("This function failed")
}
func main() error {
//
result := add(0, 1) catch (err) {
return err
}
log.Println("result:", result)
}
Idea:
If a function returns an error you must have to "catch" it. No more if err != nil
functions in C, C#, C++, Rust, Javascript, and Zig can only return 1 value. The problem was "how can you stick an error in there?" Go did the most obvious (and least creative thing) and just allows you to return multiple types. It's simple, and boring, just like go. I want to keep that spirit. It also forces "one way" of handling errors. Not sure of this syntax' implications but I like it.
It is not a "try catch" but uses the "catch" keyword which should be very familiar to developers.
Rust's approach is not pretty, but it works. It wraps errors and values in a Result
type. I don't really like this as much as Zig's approach.
You could also ignore errors by the following
func add(a, b int32) (int32, error) {
return errors.New("This function failed")
}
func main() error {
//
result := add(0, 1) catch () {}
log.Println("result:", result)
}
This makes the code ugly if the developer does NOT handle the error (which would encourage error handling)... whereas currently golang discorages it thanks to if err != nil
I'd like us to tackle error handling, and I like the catch block idea, but I do like rusts ?
operator a bit better:
func add(a, b int32) (int32, error) {
return errors.New("This function failed")
}
func main() error {
result := add(0, 1)?
log.Println("result:", result)
}
I think the main reason is the fact that Zig's solution doesn't prevent the excessive return err
all over the place, where the ?
solution still allows you to handle everything, but also have a very clean code in case of default handling.
One more example is when you want to use the result directly in something else. for example:
x := []int32{
1,
2,
add(1, 2)?,
}
or
result, err := add(3, add(1, 2)?)
WDYT?
func main() error {
result := add(0, 1)?
log.Println("result:", result)
}
From this code, what would be printed out if result was the error type?
I just don't fully understand it tbh haha
And in this example:
x := []int32{
1,
2,
add(1, 2)?,
}
what would be stored at x[2]
if add fails? Will the error be stored? Or maybe I misunderstand the function of ?
I really love the conversing and thought provoking ideas going on here
I think the main reason is the fact that Zig's solution doesn't prevent the excessive return err all over the place, where the ? solution still allows you to handle everything, but also have a very clean code in case of default handling.
Looking back at what you said, I thought about always having to return err
and your right. So maybe by default catch
just catches any errors and returns them:
func main() error {
// if error caught then it gets returned immediately
result := add(0, 1) catch
log.Println("result:", result)
}
WDYT?
That's exactly the ? operator 👍 in case of an error return it immediately.
func main() error {
result := add(0, 1)?
log.Println("result:", result)
}
Is identical to
func main() error {
result, err := add(0, 1)
if err != nil {
return err
}
log.Println("result:", result)
}
In the other example
x := []int32{
1,
2,
add(1, 2)?,
}
I guess it transpiles down to this:
temp, err := add(1, 2)
if err != nil {
return err
}
x := []int32{
1,
2,
temp,
}
If you agree we can push it to the spec readme
How would this work if I change the signature of the add() function
func add(a, b int32) (int32) {
return a+b
}
result := add(0, 1) catch (err) {
return err
}
If the catch part ignored, or does it give a compile error?
Compiler error for sure.
Again, this line result := add(0, 1) catch
(or this one result := add(0, 1)?
, depending on the syntax we choose) would actually transpile to if result, err := add(0, 1); err != nil { return err }
. if add
does not return an error, it wont compile.
More formally:
Inside a function func1
, the expression var1, var2, ..., varN := func2(...)?
is first validated so that:
func1
haserror
as its last return valuefunc2
must have N+1 return values, and the last one must be an error.
Then it should be transpiled to:
if var1, var2, ..., varN, err := callExpression(...); err != nil {
return zero1, zero2, ..., zeroM, err
}
where zero1-zeroM
represent the zero values of the return types of func1
.
Could the transpiler be "that intelligent" when there is no error in the signature, it will not generate that if statement?
(but perhaps only give a warning about unnecessary catch instructions present?)
For sure, it's for us to implement so we can definitely go either way.
However, i do think that compilation error is more strict, and aligns well with how go behaves in general (for instance, compilation error for unused variables, instead of a warning).
I think it's mostly important during refactors and changes. For example, if i have the following code:
func fetchAndPrint() error {
value := fetchData()?
fmt.Println(value)
}
func fetchData() (string, error) {
// do some IO operation
return "", nil
}
And I want to refactor so that fetchData does not return error anymore:
func fetchAndPrint() error {
value := fetchData()?
fmt.Println(value)
}
func fetchData() string {
// non-IO operation
return ""
}
now fetchAndPrint
would still compile, only give a warning on redundant ?
operator. I think it's important to give an error saying "hey, this thing does not return error anymore, please make sure to address it as well". Note that now fetchAndPrint
can and should be further simplified to:
func fetchAndPrint() {
fmt.Println(fetchData())
}
func fetchData() string {
// non-IO operation
return ""
}