Live data from Hacker News

Away from Exceptions: Errors as Values

humanlytyped.hashnode.dev

71–80 of 145 posts

Re: Away from Exceptions: Errors as Values

#71
post #51
post #45

Earlier quoted context omitted.

A null doesn't contain any information about what went wrong and unless you religiously check your objects for null values at every turn you just turned a clear stack trace into a search for waldo at the international waldo impersonators meetup.

Note that in Kotlin the return type is `Int?`, not `Int`. You can't forget to check for null because the compiler enforces it. To your first point: Another example in the design space would be Rust which works very similarly to Kotlin but returns more information in the failure case.

> Note that in Kotlin the return type is `Int?`, not `Int`.

How does that fare with unnecessary boxing of primitives?

Re: Away from Exceptions: Errors as Values

#72
post #71
post #51

Earlier quoted context omitted.

Note that in Kotlin the return type is `Int?`, not `Int`. You can't forget to check for null because the compiler enforces it. To your first point: Another example in the design space would be Rust which works very similarly to Kotlin but returns more information in the failure case.

> Note that in Kotlin the return type is `Int?`, not `Int`. How does that fare with unnecessary boxing of primitives?

If the function isn't total (as in: for every string there is an int) then the boxing is necessary, no?

Re: Away from Exceptions: Errors as Values

#73
post #9

Personally, i really like having multiple return values, since being able to give a function multiple inputs but only being able to return a single thing always felt weird - if your require any metadata in a language like Java, then you'd have to come up with wrapper objects and so on. That said, i really dislike the following from the article: if (error) { // you can handle the error as you see fit // you can add mo…

> The compiler should force you to handle every exception in some way, or to check for it. This is the single most unproductive mis-feature a language could have for me. Programming is already a tedious excercise of wrangling your thoughts into an alien form the computer can understand. You want, on top of everything else, the computer to refuse to run your program at all, unless you explicitly handle every possible…

> You want, on top of everything else, the computer to refuse to run your program at all, unless you explicitly handle every possible edge case?

Precisely!

Even better - let the IDE suggest to you all of the possible exceptions and when you're feeling lazy or are hacking away at a prototype, either let it add a "throws SomeException" to the method signature and make it someone else's problem up the call chain, or just add a catch all after you've handled the ones that you did want to handle!

After all, none of us can recall the hundreds of ways network calls can get screwed up, but we're pretty sure what to do at least in a subset of those, but we'd also forget about those without these reminders. Not only that, but when you're writing financial code or running your own SaaS, you'll at the very least will want your error handling code to be as bulletproof as the guarantees offered to you by your language's rigid type systems.

Then, when you've finished hacking together your logic, your instance of SonarQube or another tool could just tell you: "Hey, there are 43 places in your code where you have used logic to catch multiple exceptions" and then you could review those to decide whether further work is necessary, or whether you can add a linter ignore comment to the code explaining why you don't want to handle the edge cases, or just do so in the static code analysis tool, so all of your team members know what's up.

Alternatively, if you're just writing something for yourself, just leave it as it is, knowing that if you'll ever need to publish your code for thousands of others to use, then you probably should go back to those now very visible places and review it.

So essentially:

  /** 
    * Attempts to load a Sprite from a file. You can then use the instance to display it on screen.
    * @param file This is the file that we want to load the image from. Use relative path to "res" directory.
    * Our engine loads PNG files and technically can also load GIF files because someone hacked that functionality together in an evening. 
    * That's kind of slow though, so we should use PNGs whenever possible. See ENGINE-33452 for more details.
    * @return A Sprite instance that you can pass to the rendering logic to put it on the screen, or alternatively process the loaded image in memory.
    */
  public Sprite loadSprite(@NotNull File file) throws SpriteGenericException, FileSystemGenericException {
    try {
      return FileSystemSpriteLoader.loadPNG(file);
    } catch (ImageWrongFormatException e) {
      wrongImageFormatLogger.warn("We found a " + e.getActualFormat() + " format file: " + file.getPath(), e); // the art team should have a look at this
      if (e.getActualFormat().equals(ImageFormats.GIF)) {
        return FileSystemSpriteLoader.loadGIF(file); // TODO unoptimized call because we needed GIFs for ENGINE-33452, remove later
      } else {
        throw SpriteGenericException("We failed to load sprite from file: " + file.getPath() + " because of wrong format: " + e.getActualFormat(), e);
      }
    } catch (SpriteCorruptedException e) {
      brokenImageLogger.warn("We found a corrupted sprite in file: " + file.getPath(), e); // maybe the pipeline is broken again?
      throw SpriteGenericException("We failed to load sprite from file: " + file.getPath() + " because of image corruption", e);
    } catch (Exception e) { // TODO ENGINE-44551 handle the file system access cases later once the API is stable and we know how it'll work on Android
      throw FileSystemGenericException("We failed to load sprite from file: " + file.getPath(), e);
    }
  }
I prefer software blowing up in predictable ways as opposed to doing so unexpectedly. Even Java is vaguely close to being what i'm looking for, however unchecked exceptions simply isn't acceptable from where i stand.

Re: Away from Exceptions: Errors as Values

#74
post #34

Earlier quoted context omitted.

> For example, Java's parseInt [1] throws a NumberFormatException if the string can't be parsed. IMHO this is terrible design. It's unergonomical design, but it's the _correct_ design: the method is declared to return an int, and it can't fulfill its promise: throwing an exception is the right thing to do.

But the mechanism is just wrong; Exceptions are heavyweight and should only trigger with unexpected issues, bugs that a developer wants to see a stacktrace for. I mean in this case you could consider it developer error; a developer tried to parse an integer without first validating the input and checking if it COULD be parsed. But it's normalized to just "let it crash", instead of writing additional pre-check code. W…

So here's a pattern neophytes often end up implementing:

   if (isValidInput()) {
     parseInput();
   }
It seems like a good idea but it's not, for several reasons:

1. It requires an explicit second step. Worse, the behaviour of the second step may be undefined if the first step didn't take place. Either way it's just error-prone;

2. While this may be correct for a static string with static rules, what if this isn't stateless? You've now introduced a race condition;

3. If each step requires context (eg options) you need to correctly pass them to both. This is another potential source of error; and

4. You've created what's likely an artificial differentiation between valid and invalid input. What happens when that changes and you need to distinguish between invalid (not a number) and invalid (number out of range)?

(2) is particularly common in security contexts. You'll see this pattern as:

    if (userCanEditPhoto()) {
      editPhoto();
    }
Usually you can't do this, as in the primitives won't be there. This is for good reason. Engineers should instead change their mindset to implementing these things as an atomic action that gives reason for failure. Exceptions are one version of this. They're just bad for other reasons.

Re: Away from Exceptions: Errors as Values

#75
post #34
post #30

I'm firmly in the camp that believes that exceptions are a false economy. The post links to an "Exception Smells" post that doesn't mention one of my pet peeves: exceptions as control flow. For example, Java's parseInt [1] throws a NumberFormatException if the string can't be parsed. IMHO this is terrible design. As a side note, checked exceptions are terrible design. I wrote C++ with Google's C++ dialect where excep…

> For example, Java's parseInt [1] throws a NumberFormatException if the string can't be parsed. IMHO this is terrible design. It's unergonomical design, but it's the _correct_ design: the method is declared to return an int, and it can't fulfill its promise: throwing an exception is the right thing to do.

To me this is not "ecxeptional" at all, as it is easy to call that function with a non number and it should return "normally" that the input was not an integer. I pretty much prefer the rust Result or C++'s expected.

Re: Away from Exceptions: Errors as Values

#76

Earlier quoted context omitted.

> Personally, i really like having multiple return values, since being able to give a function multiple inputs but only being able to return a single thing always felt weird - if your require any metadata in a language like Java, then you'd have to come up with wrapper objects and so on. MRV is nice and useful, and “error as value” languages usually have ways to return multiple values (usually in the form of tuple),…

Does a panic count as "handling" the error? I actually agree with Rust's choice here. You, the programmer, know whether some particular error is something you can cope with or not and it's appropriate to panic in the latter case. Where you draw the line is up to you, in a ten line demo chances are "the file doesn't exist" is a panic, in your operating system kernel maybe even "the RAM module with that data in it phys…

> Does a panic count as "handling" the error?

Undeniably? Fundamentally the language proposes, the developer disposes[0] and short of Rust being a total language, panics were going to be a thing.

So while one can argue that the ability to panic should not be so prominent, it's certainly an error handling strategy which was going to be used anyway, is perfectly valid (in some situations), and is convenient when you're designing or messing around.

Hell, even ignoring an error is a perfectly valid handling strategy, and indeed pretty easy to implement, just… explicit (though not the most visible sadly, it's much harder to grep a `Result` being ignored than one being unwrapped or expect-ed).

The important bit is that Rust warns you about the error condition(s), and lets you decode on how to handle it.

[0] though there are panicing Rust APIs where it doesn't just propose

Re: Away from Exceptions: Errors as Values

#77
post #6

It's good to see so much focus on errors. They are essential when trying to build resilient systems. But our approaches are still very immature. First, to make it clear, this article appropriately points out that exceptions are still necessary and relevant. I disagree with some of the use-cases given, but it's important to recognize that exceptions should still exist in programming languages. Joe Duffy's article abou…

> when you start introducing other factors like how to report the errors publicly to a non-technical user, maybe in different languages, or whether to log it or send it who knows where, whether to trace or not, how, how to deal with duplicates or similar errors... I’ve tried searching for articles that talk about people deal with this in the context of web apps but have found it difficult to find content. It’s a tric…

I recommend again reading the article I linked to. The answer to the first part is: this should be an exception (or abandonment, as the Midori team called it). This is an error in the logic of the code, it's a programming error (even if it's due to later changes or whatever). It's an error that needs to be fixed in the code, not "recovered from".

Now, you can also catch exceptions, indeed. You could have your app catch all exceptions at the root level or wherever you think it's appropriate if your code is modular enough. Once you have caught the exception, you could silence it, as a lot of software does, and pray for the best... or be a bit more serious. If it was me, I would notify the user: "There has been an unexpected error". I would also append the technical info in a "technical details" section or something. And I would also provide a link to let the user report the issue easily. I'm kinda against hidden automatic reports for privacy-related reasons, as errors might sometimes contain sensitive data too, but it would really depend on the application.

There are many ways to make this more robust. Check for report dups on your side, or have some dynamic code to check the status of a specific error to provide the user with even more information, or even silence the error completely or whatever. But all this takes much more work, and it's really dependent on the application you are writing and how entreprisey you are willing to go. Crashes are not nice, data loss is not nice, but neither is corrupted data or subtle bugs due to errors silenced for the sake of the peace of mind of your users. You have to decide what's the right balance based on the type of program you are writing. Indie videogame? Crash as soon as possible, ask nicely for reports, get bugs fixed fast. Editor where a lot of data might be lost if you are lousy with exceptions? Definitely go out your way to auto-save separately if possible before crashing, and let the user know how to try to recover its data and how to get assistance. Non-critical webapp? Just let the user know something unexpected has happened, and allow to report and assure you will look into it soon. It always depends.

EDIT: I missed the most critical part, so I'll add it now... When communicating errors to users, the most important part is properly handling their frustration, not the logging method or the technical details included or anything else. If you have "few users", make sure they have a way to get in touch, and make sure they get a fix or a decent explanation of what's going on, let them know when it will be solved or what can they do meanwhile. Errors happen, but people are most often very understanding as long as you are there and don't leave them alone with their frustration. If you have too many users for that... good luck to you.

Re: Away from Exceptions: Errors as Values

#78

> Programming with exceptions is difficult and inelegant. Learn how to handle errors better by representing them as values. Funny how exception were invented because handling errors as values was considered to be tedious. And now, more and more languages are going backward.

I think it's less strange than you think. In most languages that use errors as values, the tediousness is being directly attacked instead of trying to dodge around it. Haskell, in many of its uses, cleans up the tediousness so thoroughly that the code written using errors as values can be almost indistinguishable from code written using exceptions, and yet, nevertheless, the errors are values and no exception machinery is being deployed.

It has been a general trend in pragmatic programming languages in the past couple of decades. Another huge example, in my opinion, is in typing. Static typing in the 20th century was terrible. Tedious, broken, and missing a lot of its value. So a lot of languages were written that basically amount to a "screw that, we're not using types", and they became very successful. But in the 21st century, a lot of work has been done directly attacking the tediousness and problematic aspects of using static types, while also getting more value out of them with safer languages that more pervasively enforce them and make them more reliable, thus more useful, etc. So we're seeing a resurgance of the popularity of very statically-typed languages... but it's not "moving backwards" because it's not the same thing as it used to be.

Much like I don't expect dynamic languages to entirely go away, I wouldn't expect exceptions as we know them to go away either. But I expect "errors as values" to continue attracting more interest over time.

In fact, as test34's sibling post sort of observes, there's some synergy between these two trends here. Making strong typing easier has made it easier to have strongly-typed, rich values that can be used as error values and used in various powerful ways. Now that there are languages where it's much easier to declare and fully exploit new types than it used to be, it's much easier to just go ahead and create a new error type as needed for some bit of code without it having to be a big production.

Re: Away from Exceptions: Errors as Values

#79
This took a while for me to get used to coming from Java/Python to Go but I'm very much a convert now - or at least it makes perfect sense for the sorta of Go services we write. It always forces me to think, can this thing fail in normal operation or is this exceptional. If former, it's an error value that eventually should be returned to the client in some form. If latter, it's a panic and I'll see it in Sentry and know I probably have something to fix.

Re: Away from Exceptions: Errors as Values

#80
post #8

"Which program is easier to read?" For me it was the second. Am I the only one ?

Agree and it could be easier to read in my opinion if you deal with the exceptions first. For example.

    let v = Number.parseInt("a3", 10);
    try {
        if (Number.isNaN(v)) {
            throw new Error("NaN");
        } else if (v > 3) {
            throw new Error("gt 3");
        }
        v += 1;
    } catch (error) {
        v = 3;
    }
    v += 1;

Or writing a "guard function" that throws ...

    function throwIfNaNorGt3(v) {
        if (Number.isNaN(v)) {
            throw new Error("NaN");
        } else if (v > 3) {
            throw new Error("gt 3");
        }
    }

    let v = Number.parseInt("a3", 10);
    try {
        throwIfNaNorGt3(v);
        v += 1;
    } catch (error) {
        v = 3;
    }
    v += 1;
Post reply on HN