← Back to context

Comment by 0127

2 days ago

IMO this is where languages like Rust shine. The design patterns built into the language for handling errors (Result) - force you to handle errors / function results with sentinels, null pointers, or errnos are so much more error prone than handling a Result<T,E> using a match statement.

crypt() returning `*` is insane especially since these kinds of sentinels are all over /etc/passwd

I guess this is where the culture around rust shines. You can write crypt that returns a string in rust just fine, just like you can write crypt that returns a tagged union in C. Cultural differences and syntactic sugar change which version is idiomatic.

I suppose if people started writing lots of code that crashed on error in Rust some enterprising developer might improve the function to always return a non-error value and set it to ‘*’ in error cases.

  • The culture is downstream of the design IMO. In general, people write the API that is obvious. In C, returning a string is obvious, while defining a custom OptionalString type is high friction. In Rust, returning Option<String> is as easy and obvious as returning a String.

    • In a C function returning a string, two or three idioms are common for signalling an error.

      One is to return NULL for errors.

      Another is to make the string result a parameter and use a return code to signal error.

      A third, less common but still out there, is an error parameter.

      Failure to observe these is on the caller. Forcing the caller to consider the error is probably where rust shines.

      But a good C programmer is thinking about these idioms when they encounter a function signature for the first time, or they write one.

  • All the standard Rust libs use results. The standard C libs use errno, return 0, return -1, return EOF, return nullptr, segfault, or of course UB. Other libs dream up things like returning "*". C++ has exceptions in some areas but not others, and if you turn them off, they're segfaults. I actually like C, at least more than C++, but the error handling is the worst part.

  • Sure, you could write deliberately bad code in Rust by unwrapping everything everywhere, but the point is that it is a distinct choice to do so. You can’t forget to handle errors - you have to make the choice to disregard them. If you forget to check for NULL in C it won’t complain and it’s dependent upon you to remember to do so.

always has amazed me that there was so much hate for java checked exceptions where handling errors ought to be the norm.

  • In the vast majority of programs, errors and non-results are irrelevant at the call site. You want to program for the happy path and not wrap every single call with error checking code or `try/catch/finally`.

        h = file_open(path)
        data = file_read(h)
        file_close(h)
        return data
    

    is the code you want to read. This essentially is:

        return file_open(path).success(h -> h, file_read(h)).success(h, data -> file_close(h); data)
    

    A language with thoughtful error handling primitives will support such code.

    • > You want to program for the happy path

      This is the root of a lot of the evil. Yes, humans want to program for the happy path. It is a lot more fun. However, that bias leads to so much lack of diligence to properly deal with the unhappy path.

      4 replies →

  • The question is _where_ the error needs to be handled. It's almost never directly at the call site. And then there's a few issues with checked exceptions and Java language things (RMI, closures, etc) which make checked exceptions cumbersome. An exception (checked or not) might be worse than sum types, but they're still a thousand times better then returning an error in-band.

    • > The question is _where_ the error needs to be handled. It's almost never directly at the call site.

      Which is why exceptions in Java can bubble up the stack? You don't have to catch it in a particular method you can let it go to the caller of the method, and so on.

      1 reply →

  • Better design breeds better habits. In Java, you can end up handling a DNS exception 20 layers up the stack, even if your call never actually triggers a DNS lookup, simply because some method deep in the call stack declares it as possible. The culture is to propagate or wrap exceptions rather than translate them into domain-specific errors near the source.

    Rust makes handling and mapping `Result` locally more ergonomic, even though `?` makes propagation easy too. Rust, however, also makes it easy to lose the context during propagation and end up with useless `Error`.

  • I agree but I think was also because Java didn't have sum types (or still doesn't? years I haven't read Java) and rethrowing is super easy, while Rust kinda nudges you into the right direction (if you need to rethrow at all, think if you want to rethrow verbatim or wrap with a semantic error type).

    Right idea at the wrong time.

    • yep, 90% ergonomics in my view. If it had its time again with modern ergonomics thing could look very different.

  • They were overused, but some actually make sense, such as the IOException and SQLException

    • The problem with them all always comes when the ever-steady march of abstractions, features, and edge cases results in one routine throwing the same two error classes but in different places, and how you handle them is different despite being the same wrapper. Add a thousand developers and several years of churn, and poof you get extremely complex bugs that result from errors seeming to come from one place, but actually coming from another.

      Having been on the receiving end of debugging that a few times, I no longer mind the in-band error handling, although admittedly I'm inexperienced enough to be confident its better. Just for the random code I've had to read where I just need to know exactly how something I didn't write and will never read again works, and whether the error is handled and how, i love reading in-band error handling, even if I too dislike the way an error laden routine must be written. Over time I've come to appreciate elegance "in the large" as opposed to the code I see in any individual function.

  • I always thought the ideas was great, but type system was holding it back. If you could have expressed that your thrown exceptions are the same as some other method's (e.g. a method of a class given as a parameter), and some other classes, then it could have been a lot less annoying. Powerful, even! But Java did take its sweet time to get even type parameters.

    I have written very little Java, though, so this is more of an armchair analyst position.

  • Amen, people saw the symptoms of "needs more syntactic sugar" and misinterpreted it into "rigorous error handling is bad".

    The same way that layered code should react/wrap/translate when it comes to return types, it should do the same for thrown exceptions, and checked-exceptions helps you do that. They're both results of computation.

Unfortunately this is also an area where a Rust programmer would likely just reach for the platform libc, because you really want to be compatible with exactly how the platform is hashing passwords. Once you know about the inconsistency, you can make predictable wrappers around it, but platform legacy is gonna keep throwing these curveballs.

Yeah too bad your adult-diaper language can't even host itself on most of the architectures where musl is relevant.

Another Rust self-suck session rendered completely irrelevant. Again.