1. 25
    1. 7

      Congratulations on the project and a great blog post.

      You mention that you do not love the verbosity of errors, and give an example.

      You are not wrapping returned errors, but pass them up the stack as is. This is not a problem only because your application is tiny, database wrapper never does more than a single operation within a method and call stack extremely shallow. If you would have a complex schema and method GetLists would make several queries, not wrapping error would cause issues. You could not tell where the error comes from.

      http.Error(w, err.Error(), http.StatusInternalServerError) is probably never wise to do. Why would you leak out internal application information? What if there is some sensitive data in the error string? What should the user do with the error information anyway?

      If you want to always return 500 on error (which is a great oversimplification or error handling) you can use a different handler notation together with a wrapper.

      func x(fn func(w http.ResponesWriter, r *http.Request) error) http.HandlerFunc {
          return func(w http.ResponesWriter, r *http.Request) {
              if err := fn(w, r); err != nil {
                  log.Printf(err)
                  w.WriteHeader(http.StatusInternalServerError)
              }
          }
      }
      

      If you will handle your errors correctly (i.e. return ErrNotFound), your can improve x wrapper to return a proper status:

      switch err := fn(w, r); {
      case err == nil:
          // Respones was written.
      case errors.Is(err, ErrNotFound):
          w.WriteHeader(http.StatusNotFound)
      default:
          w.WriteHeader(http.StatusInternalServerError)
      }
      

      Another approach could be to make your errors implement http.Handler interface in order to write error information themselves. This is not the direction I would recommend, but since your project is small in scope an experiment might not be bad :)

      type errNotFound struct {
           entityName string
      }
      
      func (e errNotFound) Error() string {
          return e.entityName " was not found"
      }
      
      func (e errNotFound) ServeHTTP(w http.ResponseWriter, r *http.Request) {
          w.WriteHeader(http.StatusNotFound)
          fmt.Fprintf(w, "%s was not found", e.entityName)
      }
      

      Since you should wrap your errors, you will need a helper function to extract the right one from a chain.

      1. 3

        I wrote a thing about how I handle errors in Go. The TL;DR is I have an error helper that lets me wrap errors in a status code and user message. If an error makes it to the top level handler without having been wrapped in a status code, the code defaults to 500 and the message defaults to Internal Server Error. I think it’s a pretty good system!

      2. 2

        These are good points, and thanks. Yeah, you’re right that I probably shouldn’t return err.Error() directly to the user – it was definitely a shortcut to avoid thinking about it for this project. :-) I’ve updated the post and code to avoid this.

        Regarding passing errors up directly: yes, it’s usually fine in a tiny app because as you say, the call stack is shallow. In languages with exceptions, the stack trace is always attached to the exception, so you get that “for free” (not in compute cost, but in developer time). I know there are libraries for Go which wrap errors and record a stack trace in the same way, but I haven’t used them. In practice for larger apps, I’d use fmt.Errorf("...: %w", err) or other forms of wrapping.

        Nice idea with the wrapper that wraps an error-returning function: I quite like the looks of that pattern. I might even play with it in this project on a branch to see how it works in practice.

        Thanks again for the interaction!

        1. 4

          In practice for larger apps, I’d use fmt.Errorf("...: %w", err) or other forms of wrapping.

          That is definitely the right way to go, and I totally understand why you wouldn’t bother with that for a tiny app!

          In languages with exceptions, the stack trace is always attached to the exception, so you get that “for free”

          That’s true, but it’s only ever a bare stack trace with no useful context. Wrapping errors in the idiomatic Go way encourages developers to add useful context: I find Go error messages are generally much more useful for debugging than stack traces. This is especially true when working with systems across a network, where a stack trace generated on one node would be almost useless.

          There’s a nice description of error handling in Go here: Error handling in Upspin

          1. 4

            The Upspin article is good, but it predates errors.As, so it’s a bit outdated IMO. I wrote a thing that builds on that article specifically about how to create error domains in a post-errors.As world: https://blog.carlmjohnson.net/post/2020/working-with-errors-as/

            1. 2

              That’s an excellent article, thanks for sharing it.

      3. 1

        A list of suggestions:

    2. 4

      Server side go is the old fashioned way now?

      I feel old.

      1. 5

        Want to impress your boss with a super snappy, low maintenance, quickly build, trivial to install backend system which Just Works? Use go templates and old-school forms. It’s unbeatable in many respects :)

    3. 3

      Heh I made something quite similar, with almost the same values (no JS, server rendered HTML). I store the data in two CSV files, one for open, one for completed tasks. Amusingly I also don’t allow editing—if you need to edit, delete and recreate. I called mine Leaf Tasks and implemented it in Rust: https://github.com/wezm/leaf

      1. 2

        Nice, thanks for sharing. Yes, some very similar design choices! I’ll peruse the code (not that I know Rust).

    4. 2

      Everything old is new again ¯\_(ツ)_/¯

    5. 2

      random tip: if you use go templates, I highly recommend you have a look at github.com/jba/templatecheck It removes most of the edit-reload cycle when making html forms with templates.