Skip to content

proposal: Go 2: "trap" keyword for func-wise error handling #56258

Closed as not planned
@xrfang

Description

@xrfang

Author background

I am an experienced Go programmer who programmed in Go for over 10 years. I have also used many other languages such as PHP, JavaScript, ObjectPascal (Delphi), as well as loads of others which I am not so proficient or have not been using them for decades.

Related proposals

This is a proposal about Go's Error Handling. I have seen many proposals on this topic, for example: #56165 (try statement), #37141 (pass statement), most of them focus on simplifying the infamous go boilerplate:

if err != nil {
    return err //or do something else, the "handler"
}

This proposal is different in that it is a schema about default error handling, which I think may not only simplify coding, but also improve code quality in the long run.

Proposal

A new keyword trap should be added in order to define a "default" error handler for all error-generating statements, inside a func. It could be used in any of the following forms:

  1. trap &error
  2. trap func(error)
  3. trap
func Func1() (err error) {
    trap &err
    f, _ := os.Open("some file")
    defer f.Close()
    ...
}

func Func2() (ok bool) {
    trap func(err error) {
        fmt.Println("ERROR:", err)
        ok = false
    }
    ok = true
    taskWhichReturnsErr()
}

func Func3() {
    trap //or, trap nil
    f, _ := os.Create()
    defer f.Close()
    //...
}

The essence of this statement is that it automatically catches error, in the following two cases:

  1. if any statement below it generated an unhandled error (i.e. panics)
  2. if any statement below it returns an error, as the only or last return value, which is deliberately ignored by "_", i.e. f, _ := os.Open(...), or implicitly, by not assigning to any variable, i.e. f.Close().

In the Func1() example, trapped error is assigned to the err variable, in Func2(), passed to the handler, and in Func3() panic-up to the caller. It is worth pointing out that the last case, direct panic-up is especially useful, because of the trap facility is designed to automatically check for errors that is ignored by the programmer, e.g. does not check the return of f.Close(), os.Rename(), db.Exec()...

Compatibility

This feature is supposed to be 100% compatible with Go1, because it ONLY works in case the programmer does NOT handle error. Consider the following case:

func Func1() {
    trap
    f, err := os.Open()
    _ = err //even if there are errors, it will NOT be trapped!
    defer f.Close() //if Close() generated error, it WILL be trapped,
                    //unless the error is explicitly assigned to variable other than "_"
}

Orthogonality

This feature may be implemented by the compiler using mechanism similar to "defer". It may be used multiple times, and be mixed with "defer".

Further Improvement

Although not mandatory, I think the trapped err should by default include a stacktrace to make it useful, just like errors.WithStack() in the errors package. I propose the following interface:

TracedError interface{
    Err() error //the underlying/direct error
    Error() string
    Stack() []string //returns stack information of the error
}

Another reason that the error should be naturally stack-traced is that the panic/recover mechanism accept/returns an interface{}, not error. The trap mechanism may be implemented as:

func Func() {
    defer func(v interface{}) {
        switch e := recover().(type) {
        case nil:
        case *error:
            *e = trace(fmt.Errorf("%v", v)) //change "any" into error, add trace info
        case func(error):
              e(trace(fmt.Errorf("%v", v)))
        default: //compiler report error
        }
    }
}

The above sample shows one difference/incompatibility between defer and trap: trap accept an argument, while defer not.

Alternatively, another keyword might be used: trace to indicated that the trapped error should have stack information, like so:

func Func1() {
    trap func(err) {...} //this err does not have stack information
    ...
}

func Func2() (err error) {
    trace &err //this err has stack information
}

Personally, I think trapped error should always include stack information.

Costs

This proposal may not make Go easier or hearder to learn, and the cost is minimal, for the sake of the language specification.

As to the impact of the compiler or the tool chain, I think it indeed may have some work to do, especially on the tool chain, sorry I don't know which tool should be modified to add such feature, but I would expect VS Code (the language server?) should warn the programmer if it sees code such as f.Close() without assigning
its return value:

error returned by f.Close() is silently ignored, consider add "trap", or check error values explicitly.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions