News for hackers. Stuff that matters.
Re:Lies we tell ourselves to keep using Golang by kaba0 on Friday April 29, 2022 @04:47PM (#31210393)
It’s a big assumption that you can even handle the error at the callsite, or that you will actually handle it correctly there, or just write some low-effort attempt because the overall picture is more important for now, but later on you won’t notice how it is not correct and it will just silently fail.

Exceptions are in my honest opinion better on every single front. Checked exceptions would be the panacea but Java’s version is not the best due to subclassing. But overall bubbling up exceptions is the correct thing to do — in most cases a problem can’t be meaningfully handled at the call site. Let’s say you want to read from a file during a HTTP request but the read fails n methods deep. There can be any number of other error occurrences and the only sane choice here would be to return some error code to the web client requesting the site. Why do you want to handle it n times instead of only at the goal?

Re:Lies we tell ourselves to keep using Golang by j3s on Friday April 29, 2022 @05:56PM (#31211027)
consider this example - one using dynamic code with exception-based handling (ruby), and one using golang's more explicit error handling:

ruby:

  begin
    res = Net::HTTP.post_form(APIURL, ...)
    puts res.body
  rescue e => e
    # do error handling
  end
go:

  response, err := http.PostForm(APIURL, url.Values{...})
  if err != nil {
    # handle PostForm error
  }
  
  defer response.Body.Close()
  body, err := ioutil.ReadAll(response.Body)
  
  if err != nil {
    # handle ReadAll error
  }
  
  fmt.Printf("%s\n", string(body))
Now, for which errors do you handle? In golang, you can see clearly where errors may occur. In Ruby, it's unclear whether HTTP.post_form is even capable of throwing errors, much less what they are. For the record, here are a few of the many types of errors that can be thrown by the ruby code. Here are a few:

  Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, ...
there's no way to tell _where_ the error will even come from - you might get a Net:: error, you might get a Timeout:: error, hell you could even get an Errno::! What most programmers wind up doing is handling no errors at all until they occur in runtime - or wrapping every single "suspicious" statement in a catch-all begin/rescue block, even when it's not needed.

In golang, each function capable of returning an error simply returns it, and you must handle it at that point in time. It is very straightforward. In my experience, it results in much more reliable code. In Ruby, you have no idea what's coming. Do you see what I mean?

Re:Lies we tell ourselves to keep using Golang by weatherlite on Sunday May 08, 2022 @08:55AM (#31303590)
> there's no way to tell _where_ the error will even come from - you might get a Net:: error, you might get a Timeout:: error, hell you could even get an Errno::! What most programmers wind up doing is handling no errors at all until they occur in runtime - or wrapping every single "suspicious" statement in a catch-all begin/rescue block, even when it's not needed.

Been programming Ruby for 8 years and have never seen anyone do that, so no... If that block of code can really throw so many different types of exceptions (usually - it can't) you can always catch StandardError.

Re:Lies we tell ourselves to keep using Golang by kaba0 on Friday April 29, 2022 @06:14PM (#31211193)
I’m not familiar with Ruby, but in java you can just specify the try-catch block’s scope as explicitly as you want.

``` try { var response = http.postForm(APIURL,) } catch (Exception e) { # handle PostForm error }

try { var body = ioutil.ReadAll(response.Body) } catch(SomeSpecificExceptionType e) { # handle specific exception differently } catch(Exception e) { # run for every other kind of error }

If a given method is defined as `void postForm(…) throws IOException` you have to handle the exception at the call site, or mark the containing call itself with a similar throws clause.

I really don’t see anything not handled by these. Also, the defer part can be done by a try-with-resources construct that will clause the resource in every case, whether exited without exception or with one.

Re:Lies we tell ourselves to keep using Golang by mumblemumble on Friday April 29, 2022 @09:05PM (#31212404)
For reliability, the absolute best discipline I've ever experienced, is "Either handle the error at (or very close to) the callsite, or crash the process." But only if you make sure that everyone on the team understands that (A) this is the only way to do it, and (B) there is no other way to do it, and (C) you will do it no other way.

I think because it sets up the right psychological incentives. Because almost any option other than those two ends up looking like, "Hmm, I don't know how to handle this, but maybe someone else will, so let's bubble it up and hope they do," in practice. It's just the way we humans are.

And that sets up an intractable moral hazard situation. It creates this middle ground where two different owners of two different parts of the code can finger-point and blame and try to pass the buck. And, knowing that (though probably only unconsciously), they won't just become sloppier about handling errors. They will become sloppier about allowing errors to happen in the first place, and doing things that make it harder to handle errors. And they won't even know they're doing it.

That's what I think of when I read your hypothetical, "Let’s say you want to read from a file during a HTTP request but the read fails n methods deep." That sounds to me like code that's been structured in a way that, on this team I was alluding to above, probably wouldn't have happened in the first place and definitely wouldn't have made it past code review. The pushback would be something along the lines of, "Why on earth are you doing both file and network I/O within a single business operation but at two very different depths in the call stack? Did you even notice what you were doing here?"

Re:Lies we tell ourselves to keep using Golang by prirun on Saturday April 30, 2022 @01:25PM (#31217566)
> For reliability, the absolute best discipline I've ever experienced, is "Either handle the error at (or very close to) the callsite, or crash the process."

Here's a counterexample from HashBackup (I'm the author):

A filesystem is being backed up. Some weird error occurs, let's say it tries to read an extended attribute and gets some goofy error that raises an exception and it's not in a try/except. That error bubbles up to the try/except for "save a file", prints the exception message, and continues by backing up the next file.

Is it more reliable to crash when reading an extended attribute, aborting the whole backup? Or is it better to just tell the user about the error, finish the backup, fail at the end of the backup with a non-zero exit code, and report it in an email?

At the time of failure, the only way I can see that the error could have been handled at the call site would be to return an empty extended attrbute. To me, that's more like the "silent but wrong" solution. And there are many of these situations for saving a single file: get the flags, read the extended attribute, read the permissions and times, figure out if it's sparse, etc. Instead of having error handling at each of these steps, they can have no error handling and just raise exceptions and the high-level "save a file" handler will do everything.

Of course, along the way there are some low-level try/except blocks around code that needs cleanups, like:

  fd = None
  try:
    fd = os.open(...)
    do some stuff
  finally:
    if fd != None:
      os.close(fd)
But you have the same thing with defers in Go.
Re:Lies we tell ourselves to keep using Golang by kaba0 on Saturday April 30, 2022 @02:09AM (#31213781)
I just gave you an example from the top of my head. Here is another more realistic one, which occurred to me during the time I wrote a toy JVM: a Java class file contains a constant pool with different kinds of types. During the loading process this pool can be references by other entities as well, but they require the necessary type. Assuming verified byte code, this will be the case, but if we care about verification as well then you will quickly get into the situation where you simply can’t handle “during the loading of this class’s nth method, one of the constant pool references were of incorrect type”. It can’t be handled at method loading (or even more nested), and panicking is not the correct behavior (you only don’t want to load the given class). So you have to manually bubble it up for no benefit.
Re:Lies we tell ourselves to keep using Golang by mumblemumble on Saturday April 30, 2022 @11:10AM (#31216622)
I could believe that, but implementing JVMs is such a very specific use case, far removed from what most programmers do day-to-day. Not just because it's a VM. Because you're implementing a pre-existing specification, so you're not at liberty to make your own design decisions around something like classloader semantics. It feels like a red herring example to me. "Here's how we should think about dealing with error conditions in 2022." "But what if I've got a spec that forces me to do it 1994-style?"
Re:Lies we tell ourselves to keep using Golang by kaba0 on Saturday April 30, 2022 @11:29AM (#31216754)
There is nothing inherent to JVMs here, it is just the all around most common error type. It simply don’t make sense to handle many type of errors at call site, because it misses the big picture and for these, return types are not a good fit.
Re:Lies we tell ourselves to keep using Golang by mumblemumble on Saturday April 30, 2022 @11:40AM (#31216855)
So, the idea is basically that of the impureim sandwich: push everything that can fail out to the periphery. If it's difficult to handle errors at the call site, it's a sign that the call site is in the wrong place.
Re:Lies we tell ourselves to keep using Golang by lanstin on Saturday April 30, 2022 @04:48PM (#31219180)
With the basic idea that you should never crash due to something outside your process call stack (whether that be disk full or network problems or bad messages or bad values in a database) and always abort immediately with an email and stack trace to yourself or equiv on internal logic errors.
Re:Lies we tell ourselves to keep using Golang by AnimalMuppet on Friday April 29, 2022 @05:00PM (#31210543)
But people write "low-effort attempts" at exception handlers all the time. So using exceptions instead of error values doesn't actually change the situation.
Re:Lies we tell ourselves to keep using Golang by kaba0 on Friday April 29, 2022 @05:57PM (#31211039)
Defaults matter. Exceptions give me the question whether I want to worry about this piece of code here, or just go on and be reminded that it is actually important at runtime with an unmissable runtime error which has exact character number for the origin of the mistake. Even Java’s checked exceptions can be just added to the method signature.

(Ab)using Result types without good syntax for dealing with the very common case of “handle it higher up” (Rust’s ? macro is ok, not perfect but it is a much better compromise) is coming from a wrong angle imo.