Skip to content

Instantly share code, notes, and snippets.

@urandom
Last active September 14, 2018 07:55
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 urandom/6519990ef9eb7547e888a5f2da7f1a93 to your computer and use it in GitHub Desktop.
Save urandom/6519990ef9eb7547e888a5f2da7f1a93 to your computer and use it in GitHub Desktop.
Reducing the special casing around the new error design draft

Reducing the special casing around the new error design draft

The error design draft is a good step in simplifying both the reading and writing of variety of code. I was even surprised that it efficiently allows readable handling of errors within http handlers:

func ListHandler(r Repo) {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    handler err {
      http.Error(w, http.StatusText(500), 500) 
    }
    
    data := check r.List()
    
    fmt.Fprintf(w, data)
  })
}

While this simple example doesn't show it, there are a lot of handlers in the wild that have a lot of repetitive error handling code, which not only makes it a pain to write, but also to read. And the handlers have similar LIFO semantics as defers, so its not all foreign.

In a way, even if the design draft is implemented as is, it will be a good step forward.

But it could be improved a bit.

What is the code after handler

The draft calls this piece of code the "handler" and treats it as a block of code. The reason for this is that somehow the return within it has to be explained. Its like an inline function, but without all the special magic afterwards.

But why can't it be a function, and not some block of code? What if the handler accepts two types of functions:

  1. a function that returns the same type it accepts
  2. a function that accepts a pointer and doesn't return anything

Then within, the chain semantics would be well defined. A func(t T) T would stop the chain, whereas a func(t *T) would not.

The CopyFile example would look something like this:

func CopyFile(src, dst string) error {
	handle func(err error) error {
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}

	r := check os.Open(src)
	defer r.Close()

	w := check os.Create(dst)
	handle func(err *error) {
		w.Close()
		os.Remove(dst) // (only if a check fails)
	}

	check io.Copy(w, r)
	check w.Close()
	return nil
}

This looks a bit more familiar. Now, lets adopt the draft's syntax as a proper syntax for defining anonymous functions [with type inference]:

t {
   return z
}

(t, u) {
   return z
}

// Or without inferred types
(t T) Z {
	return z
}

(t T, u U) Z {
	return z
}

Now, the proposed draft code will fit within a more familiar grammar:

func CopyFile(src, dst string) error {
        // err is inferred as (err error) since the function returns error
	handle err {
		return fmt.Errorf("copy %s %s: %v", src, dst, err)
	}

	r := check os.Open(src)
	defer r.Close()

	w := check os.Create(dst)
	// err is inferred as (err *error) since the function doesn't return
	handle err {
		w.Close()
		os.Remove(dst) // (only if a check fails)
	}

	check io.Copy(w, r)
	check w.Close()
	return nil
}

Both of these functions have correctly inferred arguments, since handle only accepts two types of functions.

Finally, a very minor concern:

Do not make error a special type

The draft design elevates error to an even higher pedestal than every other interface. Rather implement check and handle to accept any type, and invoke handle only when the type is non-zero. Not that people will be using these constructs for anything other than errors, it just seems over-the-top to add such a special casing restriction.

@networkimprov
Copy link

This reads like many of the counter-proposals, so probably belongs in a different section of the wiki page...

@urandom
Copy link
Author

urandom commented Sep 13, 2018

I don't view it as a counter proposal, in fact I'm mostly for it. Wouldn't a counter proposal be something completely different?

@networkimprov
Copy link

There's a set of counter-proposals that suggest "regular functions" as handlers. And there's others suggesting arbitrary type for the handler parameter.

Many of the responses are interesting reading if you haven't already :-) A significant subset has called for a way to invoke a handler by name.

@urandom
Copy link
Author

urandom commented Sep 14, 2018

To me it seems they are suggesting handling errors a bit different than the draft in some way (either by only allowing function syntax for the handler, or having named functions used with handlers). My suggestion doesn't deviate from the design's syntax or usage (as seen from the last block of code), just places it within more familiar rules (if that makes sense)

Maybe mine should be in the "other" category

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