Skip to content

Instantly share code, notes, and snippets.

@mikeschinkel
Last active April 6, 2022 17:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mikeschinkel/66caa23ea787e9f284e495220ec85082 to your computer and use it in GitHub Desktop.
Save mikeschinkel/66caa23ea787e9f284e495220ec85082 to your computer and use it in GitHub Desktop.
Potential approach for streamlined error handling in Go

Proposal for Streamlined Error Handling in Go

This is a proposal for a way to simplify the error handling in Go.

The idea is to use attempt{} and recover{} blocks to separate the happy path from the error handling. NOTE: this is NOT exception handling as you would see in Java or PHP. More on why below.

In this proposal Go would allow the developer to omit capturing the last returned variable from a function call if and only if that call was within a attempt{} block. The Go compiler would handle providing access to the error instance via the geterror() function within the recover{} block if any function returns a non nil error as its last omitted parameter, i.e.:

attempt{
  data:= GetData()
} 

recover {
  println( "ERROR: " + geterror().Error() )
}

Instead of this which you currently have to do:

data, err:= GetData()
if err != nil {
  println( "ERROR: " + err.Error() )
}

This proposal suggests Go add a new built-in function named punt() — or it could be a statement — which would merely transfer control to the recover{} block before first setting an internal static variable to be returned by a suggested new built-in and go-routine safe function named geterror().

In addition this proposal suggests a new //go:action comment type that would be used like the following example, where the Go compiler would call a SetAction() method defined in an interface maybe named action if it was applied to the error object to set the value returned by the Action() method:

attempt{

  //go:action getting the data
  data:= GetData()

  //go:action processing the data
  data:= ProcessData(data)

} 

recover {
  fmt.Printf( "ERROR: unable to %s; %s", 
    geterror().Action()
    geterror().Error() )
}

Also there should be a built-in punt() function or statment that simply accepts an error and returns it, which when called inside a attempt{} would trigger a jump to recover{} for any non nil error:

attempt{

  //go:action getting the data
  data:= GetData()
   
  //go-action checking if data is empty
  if data == nil {
    punt(errors.New("data is nil"))
  }

  //go:action processing the data
  ProcessData(data)

} 

recover {
  fmt.Printf( "ERROR: Occurred while %s; %s", 
    geterror().Action()
    geterror().Error() )
}

And a last point is that this approach would allow using functions that return both a value and an error to be used in conditional expressions, e.g.

attempt{

  //go:action getting the data
  data:= GetData()
   
  // evaluating if data should be processed
  if ShouldProcessData(data) { // <-- this contrived func returns (bool,err)
    //go:action processing the data
    data:= ProcessData(data)
  }

} 

recover {
  fmt.Printf( "ERROR while %s; %s", 
    geterror().Action()
    geterror().Error() )
}

Please Comment

If you read this far then you are obviously interested, at least somewhat. Can you please at least provide your questions and/or impressions in the comments?

package mypkg
import (
"fmt"
)
// Example receiver showing how attempt{}-recover{} would look in use
func (c *Config) Initialize() (err error) {
attempt {
// go:action get home directory
c.HomeDir = GetHomeDir()
if c.HomeDir == "" {
throw(fmt.Errorf("home directory is missing"))
}
// go:action get working directory
c.WorkingDir = GetWorkingDir()
if c.HomeDir == "" {
punt(fmt.Errorf("working directory is missing"))
}
// go:action create initial package file
c.MaybeCreateInitialPackageFile()
// go:action load packages
c.LoadPackages(c.GetGlobalPackageFilepath())
// Set global vs local classpath
c.FileSource = LocalFiles
}
recover {
err = geterror() // or just error()?
err = fmt.Errorf("unable to %s for MyApp; %w",
err.Action(), // or just action()?
err)
}
return err
}
// action is an interface that Go would define and also look for
// when a function is called inside a attempt{} block does not provide
// a variable to accept its last return value WHEN that last
// return value implements the `error` interface. If it finds the
// interface it would look for a preceding `// go-action <action>`
// comment and then call the SetAction() method on any error that
// is returned passing to it the "<action>" string after which Go
// would store the error into a location that the new built-in
// and go-routine specific geterror() function can return and then
// transfer control to the `recover{}` block.
type action interface {
Action()string
SetAction(string)
}
// This is how the internal Go func that could be used inside a
// attempt{} statement would be implemented. Go should not compile
// any code that uses punt() unless that call is within a attempt{}.
// punt() only returns an instance that implement `error` that was
// passed to itand the Go compiler handles jumping to the recover{}
// for when error objects are returned and not captured to a variable
// within the attempt{}.
func punt(err error)error {
return err
}
// Config is mermely an example interface used by Initialize() code
// example above.
type Config interface {
GetHomeDir() (string, error)
GetWorkingDir() (string, error)
MaybeCreateInitialPackageFile() error
LoadPackages(string) error
}
@mikeschinkel
Copy link
Author

mikeschinkel commented Aug 19, 2021

BTW, what triggered me to envision this is I was doing a code review and refactor for an open-source project and I was adding a lot of second parameters to return errors where the developer just ignored that an error might occur.

But when I did that every one of these:

if Foo() {
   // Do something
}

Had to be refactored to this:

value, err := Foo()
if err != nil {
   return err
}
if value {
   // Do something
} 

Which means that too many developers will simply not return errors when they should.

But if it was not as painful, then they would me much more likely IMO to return an error in the cases an error can occur.

try{
  if Foo() {
    // Do something
  } 
catch {
  err = geterror()
}
return err

Which doesn't look much better. But it does when you have many of them:

try{
  if Foo() {
    // Do something
  } 
  if Foo2() {
    // Do something
  } 
  if Foo3() {
    // Do something
  } 
catch {
  err = geterror()
}
return err

Someone who I first showed this to said "I'd just fire the developer who doesn't handle errors" to which I pointed out:

"You can't fire the developer who writes the only package for a use-case on GitHub that you really need to use, but that unfortunately ignores errors. So firing is not a panacea."

He also said "i’m not wild about throw / catch because in java then people end up not catching them and the propagate up unexpectedly" to which I responded:

"Then you did not fully read the proposal because it explicitly states this was not exception handling. 😁 This proposal would not propagate the throw() outside of a func in which it is called. It is really just syntax sugar for a goto to to the catch block and a goto just before the catch block to just after the catchblock."

Finally I said:

"BTW, this was my 5th iteration of the proposal. I started with having Go jump to an error: label when an error object returned from a func was not captured into a variable and that error object was not nil. But then I realized a simplified pair of try{} and catch{} blocks where the catch{} block would provide a better structure around the error handling/recovery section of the code."

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment