Skip to content

Instantly share code, notes, and snippets.

@kfsone
Last active May 24, 2019 06:00
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 kfsone/9a2f81f10a914871cc04d8e3d9bfa219 to your computer and use it in GitHub Desktop.
Save kfsone/9a2f81f10a914871cc04d8e3d9bfa219 to your computer and use it in GitHub Desktop.
A proposal for formalizing and inlining error handling in the Go language
// Go's informal error handling syntax introduces a lot of boiler-plate and ambiguity:
// boiler-plate: you have to capture and test the value, and you frequently want to forward it, etc
// there is also the potential for people to violate convention/norms.
func sqrt(value float64) (float64, error, error) {
// what does the second error mean?
}
func v(value float64) (error) {
// just, what ?
}
// So lets look at an actual quick example of a thin client helper function
func sqrt(value float64) (float64, error) {
if value < 0 {
return 0, errors.New("can't sqrt a negative value")
}
return math.sqrt(value)
}
// thin-client:
func f(v1 float64, v2 float64) (float64, error) {
v3, err := sqrt(v1)
if err != nil {
return 0, errors.New("v1 was invalid")
}
v4, err := sqrt(v2)
if err != nil {
v4 = sqrt(v1 + v2)
if err != nil {
return 0, err
}
}
return v3 + v4
}
// In another language, far far away, this might have simply been:
float sqrt(float value) {
if (value >= 0)
return math.sqrt(value);
throw invalid_argument("can't sqrt negative")
}
float f(float v1, float v2) {
float v3 = sqrt(v1);
try {
float v4 = sqrt(v2);
} catch (invalid_argument) {
v4 = sqrt(v1 + v2);
}
return v3 + v4;
}
// We had a LOT less room for error here. But ... ugh, exceptions.
//
// My proposal has two potential iterations. Syntax totally open for debate,
// and I've deliberately chosen evocative elements so that what seems like the
// logical default choice is an equal amongst candidates.
// The first iteration would simply be to reduce the clutter of the normal
// error handling by allowing
// if <expression>; error(...)
// to be syntactic sugar for
// if <expression> { return <args0..n-1>, <error clause> }
//
// This is specifically to address the question of "what do the other return parameters mean when there is an error?"
//
// The goal is to explicitly remove those fields from the programmers mind.
//
// Obviously there are times when it is legitimate for an error to accompany partial data, and the old
// syntax still allows for this.
v, err := call()
if err != nil; error(err)
if err == nil; error(errors.New("you can't win"))
if err != nil {
fmt.Println("This is impossible!")
return 0, err
}
// You saw some extra information in the above that you didn't notice.
//
// In the first two error cases, the author explicitly indicated intent NOT to return anything but an error.
//
// Future code analysis tools could tag/inspect this and be able to warn users when they do something like:
//
// v, err := above_func()
// if err != nil { ... non-terminal ... }
// fmt.Print(v) // warning: above_func does not populate v in an error condition
// The second iteration is "failable funcs". These are annotated, in this proof
// by suffixing a question mark.
// Failable functions *must* have one and only one error return and it must be
// the trailing return.
//
// In this proof, failable funcs have two potential syntaxes.
v1, ... vn, err := failfn?()
if err != nil; error(...)
// the error value *must* be tested, or else a warning is produced.
//
// The second syntax introduces an allowed else after a failable function
// which can be followed by either a singular error expression or a compound
// statement that ends with an error expression:
func failable?() (int, err error) {
// "else error(err)" here is effectively
// if err != nil {
// return default, v1_default, ... vN_default, err
// }
v1, ... vn, err := failfn?() else error(err)
v2, ... vn, err := failfn?() else {
log.Print("v2 was invalid")
error(errors.New("invalid argument"))
}
return 0, nil
}
// Addenda:
// 1. Precedence: What if "else" had precedence over "="?
// This should allow the following:
v1, err = failfn?(v1) else {
log.Print("v1 is bad:", v1)
error(err)
}
// to be interpreted as the contents of the else clause happening *before* v1 is assigned, and since it results in an
// error, v1 is actually never overwritten. Thus:
self.Member = self.GetConnection?() else {
// I don't have to use an intermediate value!
// if GetConnection? fails, self.Member won't be assigned to
error(err)
}
// I also wanted to consider eliding the 'err' where possible to make it less onerous, but I'm torn on making it inconsistent.
// part a: auto capture failfn?.argument(-1) -> named error variable
func failable?() (int, err error) { // if the error is named, we can auto-capture it so the user doesn't have to specify it
v1 := failfn?() else error(err)
return v1, nil
}
// I would also have liked to elide the ', nil' in returns, but this would come back as developer pain
// any time the number of arguments changes. For this to be viable, I'd want a consistency change in
// failable funcs so that you must *never* specify the error value in return in failable funcs:
func failable?() (int, err error) {
v1, err := failfn?()
if err != nil {
log.Print("failfn failed")
// return 0, err // compiler error, expecting 1 argument
return 0 // returning 2 args would be invalid, 'err' is returned implicity
}
return v1
}
// There's a C++-grade sharp edge here: the user might expect that final statement to return err = nil,
// but it would just return err.
//
// I think this can be addressed: In a callable function, return *always* returns err = nil. The only way
// to override this is with an error() expression.
return error(err) // all leading return values zeroed
// return ; error(err) // just, no.
return 0, 1, 2, 3; error(err) // familiar ';' syntax per for etc
return 0, 1, 2, 3 // } return 0, 1, 2, 3, nil
return 0, 1, 2, 3; // } return 0, 1, 2, 3, nil
// lastly: I like the explicit appearance of this, but it might not be viable:
return 0, 1, 2, 3; error(nil)
func NewConnection?(host string, port int, non_blocking bool) (socket Socket, err error) {
address := Resolve?(host) else {
log.Print("Could not resolve", address)
return error(err)
}
if port < 0 || port > 65535 {
return error(errors.New("Port out of range"))
}
socket := NewTCPSocket() {
return error(errors.New("Could not bind port"))
}
if non_blocking {
socket.SetNonBlocking()
}
// Look ma, no assignment required
socket.Connect?(address, port) else {
// a non-blocking connection may return an EINPROGRESS error
// if the connection wasnt instantaneous
if !isEINPROGRESS(err) {
error(err)
}
// Demote from an error
log.Print("Connection establishing", host, port)
// continue back to outer scope
}
return socket
// or
// return socket; error(nil)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment