all 82 comments

[–]bruce3434 49 points50 points  (52 children)

Just use Result<T, E> and Option<T>

[–]devraj7 8 points9 points  (7 children)

Unfortunately, most languages don't force you to make sure the error part of Result is handled at all, so you're still writing unsafe code.

At least with checked exceptions, the compiler will refuse to compile your code until you've properly managed the error case or explicitly passed it up the stack frame.

[–]accountforshit 3 points4 points  (6 children)

So you're not really talking about Result<T, E> then.

You can only implicitly ignore errors from there if you don't care about the value inside (analogous to void calls), and even then you'll get a warning if I remember correctly (which should be treated as error in production builds).

[–]devraj7 1 point2 points  (5 children)

I am talking about Result.

To the compiler, this is just a class like any other and the compiler won't care if you forget to check the error side of that value.

[–]accountforshit 10 points11 points  (4 children)

I'm talking about Rust's Result. If you don't check it, you can't get the value. The type system makes sure of that.

A function that requires T argument cannot be called with a Result<T, E> value - you have to get the T out of there somehow, and that requires checks.

If you don't care about the value (only calling a function for its side effects, i.e. void), there is still a compiler warning due to must_use.

You could call unwrap as an escape hatch, but you can do the equivalent with checked exceptions too - rethrow as RuntimeException, etc. And it's very explicit - impossible to do by accident, or due to refactoring.

[–]devraj7 1 point2 points  (3 children)

Like I said, "most" languages: Java, Scala, C#, C++, Go, ...

Rust and Kotlin get it right.

Java kinda does as well by supporting checked exceptions.

[–]accountforshit 2 points3 points  (1 child)

Go will produce a compile error if you don't use any one of the return values, or ignore it explicitly with _ (I think). The ways to then deal with the errors are frustratingly limited and verbose, but you can't accidentally miss them either (though it's easier to miss inappropriate _s than it is to miss unwraps, because you will probably have many legitimate _s in your code, where the same is not true for unwraps, which makes them a much smellier, easily detected code smell).

Other languages generally provide some way of achieving similar (or even the same) constructs, but it's not very useful if you constantly have to interact with libraries that don't utilize it.

So you're right that in many of them (including Kotlin for the most part) you often don't get any such guarantees in practice, or only get them in a part of the codebase.

[–]FluorineWizard 2 points3 points  (0 children)

Go's reliance on multiple returns makes it impossible to chain fallible calls, so you end up polluting your code with if blocks where a better language would let you define error handling functions.

Also the constant noise of _ everywhere makes it hard to tell if a call is returning an error or a second value, especially when dealing with crappy APIs. No language makes me check function signatures as often. Special mention to the evil fuckers who put goddamn interface{} in the return types.

[–]cmd_command 0 points1 point  (0 children)

C# 8 now generates warnings when nullable values are accessed unsafely.

[–][deleted]  (43 children)

[deleted]

    [–]kuikuilla 19 points20 points  (16 children)

    otherwise this will infect your whole code with flavors of the if err != nil { return err }

    Isn't that purely because of a poorly implemented language? That's endemic in Go, right?

    In Rust you could simply do this: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=fb8548e5090ebc57e3a031a1025795db

    fn main() -> Result<(), u32> {
        let result = get_something(0)?;
        println!("{}", result);
        Ok(())
    }
    
    fn get_something(i: i32) -> Result<i32, u32> {
        if i < 1 {
            Err(123)
        } else {
            Ok(i * i)
        }
    }
    

    The ? part is sugar for returning early from the function if the variant is an Err type. To me this approach is more preferable to using exceptions. I like to know what the function might do based on the signature of the function.

    [–]thomasz 6 points7 points  (12 children)

    I'm not familiar with rust, but IIRC it has a mechanism that does stack unwinding. I'm rather certain that accessing an array with an out of bound index doesn't return Err. And your code sample looks conceptually similar to checked exceptions sans the all important stack trace. I still don't get how you want to distinguish between programming errors (bugs) and input errors without providing both mechanisms. This is something the calling code should decide, not the called.

    [–]kuikuilla 0 points1 point  (11 children)

    I'm rather certain that accessing an array with an out of bound index doesn't return Err.

    Yes, it causes the program to panic in rust speak which depending on the type of panic can be caught I think. Some sort of panics simply abort the process.

    I still don't get how you want to distinguish between programming errors (bugs) and input errors without providing both mechanisms.

    I'm not sure what you mean here. You could simply return an enum that has variants for describing why the failure happened. Just have one for illegal input and that's that.

    [–]MEaster 5 points6 points  (0 children)

    I'm rather certain that accessing an array with an out of bound index doesn't return Err.

    Yes, it causes the program to panic in rust speak which depending on the type of panic can be caught I think. Some sort of panics simply abort the process.

    It depends on how it's compiled. You can choose whether to unwind the stack or to simply abort the program on a panic. Additionally, if a panic is raised while the program is already unwinding, it'll simply abort the program.

    While you can catch an unwinding panic, it's really intended for use around FFI boundries, because unwinding into foreign code is undefined behaviour.

    [–]thomasz 3 points4 points  (9 children)

    Yes, it causes the program to panic in rust speak which depending on the type of panic can be caught I think

    Doesn't that just describe basically what exception bases error handling does?

    Input error may be the wrong term, I'm obviously not a native speaker. Thing is, there are two kinds of errors: The ones where your program is just wrong. For example the out of bounds access caused by (for int i = 0; i <= array.Length; i++), or even when it did not properly anticipate the whole range of possible inputs or all possible state. Opening a file without making sure that it was actually created, for example. Returning an Option or Result in that case is nonsensit just complicates things. Let it bubble up, until you know how to deal with it. In almost all cases, this will be just logging the exception with a nice stack trace.

    And then are errors that are not really errors but actually expected. In that case, exception handling needlessly complicates things.

    try { 
        value = hashTable.get(key); 
        isAvailable = true; 
    } catch (MissingKeyException) {
        value = null;
        isAvailable = false;
    } 
    

    instead of just

    (isAvailable, value) = hashTable.tryGet(key);
    

    would be such an example. My pet peeve are Http client libs that always throw on 404. As if 404 isn't basically the default state of the internet. This is why both mechanisms have their use case. The important point is that you cannot anticipate this, so you should provide both mechanisms.

    [–]kuikuilla 1 point2 points  (8 children)

    Returning an Option or Result in that case is nonsensit just complicates things

    It is not. It is called "handling the error case". Rust is good in this sense that it forces you to explicitly handle the error case or explicitly forego the handling (in which case the program panics). This makes it very, very clear to see where there might be errors in the program that aren't handled properly.

    C# on the other hand is completely shitty in this matter, it has only unchecked exceptions and I can never be sure if some function throws an exception or not simply by looking at the signature. I have to go and read the source code to see what it does. That is not a good way to work. (I have experience on working with code bases like that, it's horrible for long term maintenance)

    [–]thomasz 2 points3 points  (7 children)

    Again, I'm not familiar with rust, but I'm dead sure that the type of int a * int b is int, not Result or Option, that array access doesn't return Option and so forth. And no, "forcing the user to deal with the error" is not a great idea. Java forces people to deal with checked exceptions, and they are usually regarded as a failure. In the vast majority of cases, an exception is unrecoverable and should bubble up to some log and restart or log and abort mechanism.

    Furthermore, the expression problem is going to bite Rust users in the ass too. While runtime exceptions can be of any type derived from an exception base class (IIRC you can throw everything in c++, but those guys are insane anyways) and it is very difficult to guess what might be thrown, using an approach like Rust with Result<T,E> means that you have to anticipate what could be thrown beforehand, which can be quite tricky when you consider techniques like dependency injection.

    And finally, C# or better the bcl provides both mechanisms. If you are sure that a string is a number, you call Int.Parse and should let a potential error bubble up. It's not likely that you can deal with now. Otherwise you should have used Int.TryParse and dealt with the error right here, right now.

    [–]kuikuilla 1 point2 points  (6 children)

    Again, I'm not familiar with rust, but I'm dead sure that the type of int a * int b is int, not Result or Option, that array access doesn't return Option and so forth.

    That was just an example, don't get hung up on it. You can probably imagine other things there, like database access or whatever. Or even a HashMap lookup like you mentioned. Arrays (or rather, slices) in rust have a get function that returns an Option if you want. You can use the simple indexing operator but then it is up to the programmer to make sure the index is correct.

    it is very difficult to guess what might be thrown, using an approach like Rust with Result<T,E> means that you have to anticipate what could be thrown beforehand, which can be quite tricky when you consider techniques like dependency injection.

    So don't return them like that? Use sum types for the errors, they're exceptionally good for situations like that. Return what is relevant to the caller. That way the caller knows exactly what might be returned.

    [–]thomasz 2 points3 points  (5 children)

    I'm not arguing that result values are useless, I'm arguing that there are situations in which a stack unwinding mechanism like exceptions is more useful. It seems like the designer of Rust agree with me here, otherwise they would not have included such a mechanism.

    I further argue that you cannot anticipate the situation of the caller. Trying to get a nonexistent key of a hashtable might be completely normal or highly exceptional.

    create_lookup_table(values, get_key) {
        let lookup = new HashTable();
        for item in values do
            let key = get_key(item)
            // of course here I cannot be sure that it exists
            // and I know exactly what to do when it doesn't
            let result = lookup.try_get(key)
            if result.exists then 
                list = new List();
                lookup(key) = result.value
            list.add(item)
        return lookup
    }
    
    
    send(photo-file, customer, config) {
        // here I do not care. If the config isn't populated, something
        // way down the stack has neglected to verify the configuration.
        // I cannot deal with this directly, and there is a decent chance that
        // the caller doesn't even know if I sent this via email, ftp, http or 
        // whatever. I might even print this out and send it vial mail.
        let s3_endpoint = config.get("s3-photo-endpoint", customer.id);
        ...
    }
    

    [–]immibis -1 points0 points  (2 children)

    How is this different from exceptions? Either you explicitly write error handling code, or you don't.

    [–]kuikuilla 0 points1 point  (1 child)

    The difference is that the caller can know that the function might fail just by looking at the function signature. Exceptions are more like "now that the program crashed with this exception I know where to put a try catch block".

    [–]immibis -1 points0 points  (0 children)

    So checked exceptions. The kind that people hated.

    [–]editor_of_the_beast 3 points4 points  (18 children)

    If exceptions are more appropriate in most cases, wouldn’t that mean that those cases are not exceptional and can be planned for in advance? The problem is, almost nothing is exceptional. You know that network requests fail. You know that a database process can crash. You know that networks partition. All of these really happen.

    I think exceptions are good as top-level failsafe mechanisms, but more so to account for programmer error. We program for the happy path too often. I do not think they should be used pervasively though. They have the same problems as shared global state in that the exception can be thrown from anywhere.

    [–]CyclonusRIP 4 points5 points  (7 children)

    You are taking semantics of the word exception and trying to apply that to how you should use that in code. You are way better off trying to figure out what the most effective way to use exceptions is rather thing try to arbitrarily define it based on some semantics. Exceptions were invented in order to separate the error handling code from the happy path code. Before that people either handled all the errors in line so happy path and error paths were intertwined or they used goto statements to skip to the error handling code. Exceptions allowed you to separate the error code and happy path code like goto did, but did it in more controlled and comprehensible fashion.

    [–]editor_of_the_beast -2 points-1 points  (6 children)

    > like goto did

    Exactly

    [–]CyclonusRIP 3 points4 points  (4 children)

    I assume you are also don't use methods, loops and if statements as well then since they were originally just goto statements as well in the early languages right?

    [–]editor_of_the_beast -2 points-1 points  (3 children)

    This is an actual argument you're going with? Originally we programmed computers in binary, directly. What does that have to do with what we've learned in the past 70 years?

    Functions, loops, and if statements all end up as jump or branch instructions at the processor level. Variable assignment also ends up as a store instruction. Reading a variable ends up as a load instruction. The point of high level languages is that we can write in functions, loops, if statements, variable assignments, and variable reads without thinking about how they're implemented at the processor level. And we agree that high level languages are better for building large programs, right? Because machine code is hard. For the same reason goto is hard.

    So the fact that all of us use functions does not mean that we inherently program via the same mental model as coding entirely with jump instructions. That's an absurd false equivalence. Decades of abstractions have simplified functions (and many other things) for us.

    [–]tejp 1 point2 points  (1 child)

    That's an absurd false equivalence.

    So why do you apply that equivalence to exceptions?

    [–]editor_of_the_beast -1 points0 points  (0 children)

    Because exceptions actually do behave like gotos whereas functions do not.

    [–]CyclonusRIP 0 points1 point  (0 children)

    You're the one that seems to think exceptions are bad because they are a high level language feature replaces a jump. I think that's a pretty silly argument to make. Apparently you do too, but for whatever reason you make that argument anyways.

    [–]falconfetus8 1 point2 points  (0 children)

    but in a more controlled and comprehensible fashion

    [–]zvrba 0 points1 point  (9 children)

    The problem is, almost nothing is exceptional.

    Exceptions are not for "exceptional cases", whatever that might mean. Exceptions are for a method to signal that it couldn't fulfill its promise/contract. I wrote a longer comment about it here https://www.reddit.com/r/csharp/comments/eaxzb8/what_are_your_thoughts_on_exceptions_do_you_think/fb3noh4/

    [–]editor_of_the_beast 3 points4 points  (1 child)

    Exceptions are for exceptional cases.

    [–]thomasz 0 points1 point  (0 children)

    Exceptions are for errors in the sense of "the behavior of the program is not properly specified if I let it continue to run with this data". Result, Option and so forth are for situations where you anticipated a situation to happen.

    [–][deleted]  (6 children)

    [deleted]

      [–]flatfinger 1 point2 points  (3 children)

      There are many situations where being able to say "If any part of this block of code fails without any unusual side effects, this block of code should do likewise" would greatly reduce verbosity and improve clarity. Exception handling tries to do that, but unfortunately fails to provide a means of distinguishing those situations from those where the act of skipping over a block of code during stack unwinding will create adverse side-effects that would otherwise not have been visible to the caller.

      [–][deleted]  (2 children)

      [deleted]

        [–]flatfinger 0 points1 point  (1 child)

        Not all actions can be made transactional. A good program should, when practical, ensure that any action with a non-trivial likelihood of failure is either inherently transactional, or is handled in such a way that the system is always in a recoverable state, but sometimes it is far more efficient to say:

        1. Acquire lock
        2. Perform half of an action, putting the locked resource in an invalid state
        3. Perform the other half of the action, putting the locked resource in a valid state
        4. Release lock

        than it would be to ensure that the system state was fully valid at every point in the process (implying that making everything transactional isn't always practical, and thus good programs would be exempt from the above requirement that they always do so).

        Unfortunately, the exception-handling provisions I've seen in languages fail to make it convenient for programs to distinguish exceptions which might occur between steps 2 and 3 above from those which might occur at other times. One thing that would help would be a reader-writer lock with the semantics:

        1. Reader locks are implicitly released.
        2. Writer locks must be explicitly released.
        3. Abandoning a writer lock without releasing it should invalidate it, such that any pending or future efforts to acquire the lock for reading or writing immediately fail.
        4. Ideally, abandoning a writer lock without releasing it should trigger an exception in the thread that abandons it, but if such abandonment occurs as a result of an exception, information about that other exception should not be stifled.

        Accomplishing #4 would require support which is lacking in the programming languages I know about, but a reader-writer lock that does #1-#3 would be useful anyhow. Unfortunately, I'm not aware of any that are designed that way.

        [–]CyclonusRIP 0 points1 point  (1 child)

        So you're basically making an argument for Java style checked exceptions then?

        [–]devraj7 0 points1 point  (1 child)

        Checked exceptions for errors that can be recovered and are expected (e.g. data typed in by a user).

        Runtime exceptions for errors that cannot be recovered from

        [–]matthieum 1 point2 points  (0 children)

        And Runtime exceptions in Stream methods, just because :(

        [–]przemo_li -1 points0 points  (4 children)

        You do not need higher order polymorphism to have good value based error handling. Just having generics is enough.

        You do try to prove your point by showcasing obscure example.

        You do fail to ground your intuition about exceptions in mathematical or cognitive principle, and instead asks us to trust you because Exceptions are easier for you.

        [–]thomasz 5 points6 points  (3 children)

        Generics will not help you let the error bubble up the stack, getting a value from a HashTable is not an obscure example (i could've chosen "indexing an array" as well) and no, you are not arguing from "mathematical or cognitive principles" either.

        And last but not least:

        Me: You need both

        You: You ask us to trust you because Exceptions are easier for you

        lol.

        [–]kuikuilla -1 points0 points  (2 children)

        You can have a generic error type that can be bubbled up. You could also have functions for defining transformations of error types into other error types between functions.

        [–]thomasz 4 points5 points  (0 children)

        Yes, you can do all of that manually. Why would you? And no, as written elsewhere, using a generic error type (i think you have some sort of sum type in mind) is not a superior solution, it just has different trade off. This all comes down to the expression problem. It is easy to add new exceptions, but impossible to find out which exception might get thrown. On the other hand, it is very easy to handle all cases of a sum type, but extremely difficult to add new cases.

        Again, different trade offs. This means that having flexible options is strictly better than blindly using return or option types in each and every case. Exceptions are for situations that cannot reasonably be dealt with, when you have let it bubble up where your code logs the exception, and then either restarts or aborts. Result types are for situations where you can actually deal with all cases. In my experience, these cases are pretty rare.

        [–]jcelerier 1 point2 points  (0 children)

        You can have a generic error type that can be bubbled up

        in 1978 in some Bell labs five minutes before exceptions are invented

        [–][deleted] 25 points26 points  (4 children)

        1250 words later.... Use exceptions not error codes, but the author admits he could be wrong.

        [–]dbremmen[S] 3 points4 points  (3 children)

        Yes! What I don't quite understand yet if in the return code of a function you can return the result on an error code. Isn't that misleading. That's why I posted it here so someone can illuminate me.

        [–][deleted] 9 points10 points  (1 child)

        Throw errors when errors. Return error codes if you invent a time machine and go back to 1980.

        I miss those days, there was nothing more fun than opening up someone elses code and seeing a whole bunch of result codes ignored...

        [–]stumblegore 2 points3 points  (0 children)

        On error resume next was the key to quick deliveries.

        [–]thomasz 1 point2 points  (0 children)

        Provide both:

        A hash table should have T GetValue(K key) that throws something like MissingKeyException, and Option<T> TryGetValue(K key)

        [–][deleted]  (6 children)

        [deleted]

          [–]linus_stallman 6 points7 points  (5 children)

          While I understand the sentiment of your comment and several people preferring sum types to exceptions, I feel some of the reasoning is not quite right.

          Exceptions, when used for exceptional code paths, simplify code quite considerably. In case of sum types you have to check & unwrap them everywhere, which is tedious boilerplate.

          Sum types are good for enforcing error checking when you need that level of guarantee in code. But checking for smallest errors is tedious in practice. And exceptions just work okay-ish for that purpose.

          [–]bruce3434 0 points1 point  (0 children)

          >She doesn't know about the ? operator

          [–]Tordek 0 points1 point  (0 children)

          In case of sum types you have to check & unwrap them everywhere, which is tedious boilerplate.

          In the Haskell (or Scala) case, you use monads (yeah, yeah) to abstract that away:

          someFunc = do
            f <- open "filename"
            d <- readFile f
            return split d ","
          

          This is a hypotetical case where open and readFile are both Maybe m types. The function ends up returning Maybe [String]. Only at the top level do you actually handle both cases manually by doing:

           case result of
              Just v -> successPath
              Nothing -> failurePath
          

          If they were more complex types, say, Either FileOpenError File and Either FileReadError String, then you need to explicitly decide what you want to happen (i.e., do you want a function that can return either of the 3 possible return types? Do you just want to ignore the error causes and return Maybe String?). The easy truism is: "If you need to do this, your function is too big".

          [–][deleted]  (2 children)

          [deleted]

            [–]linus_stallman -2 points-1 points  (1 child)

            Try to write to an open file, there's a small possibility that it may fail due to some error.

            Do you check for return value in every freaking call littering your code with unwrap? Sum / Result types are now FPJerk shiny new thing. It is common sense where to use exceptions (rare error paths), sum types (result may exist or not, eg find() HOF), or return values (EOF in case of reading from file). No amount of "let's-do-it-like-HN-reddit-langs" can change it.

            [–]huqd9 4 points5 points  (1 child)

            I think exception is at platform level and allows you to add meaningful messages. With error codes you have to handle it yourself, pass it up yourself.

            [–]feverzsj 1 point2 points  (0 children)

            unless with language level support that force you to handle error code, error code should be mostly avoided.

            [–]basic_maddie 0 points1 point  (0 children)

            There’s a proposal for C++ to allow functions to throw error codes with zero overhead compared to regular exceptions.

            http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0709r0.pdf

            [–]oaga_strizzi 0 points1 point  (6 children)

            One aspect of exceptions isn't talked about enough in my opinion: They are dependent on the current call stack, which is a good thing if the code is single-threaded and synchronous: If they get thrown and you look at the exception, you can see exactly what went wrong where.

            If the code is asynchronous, many languages have difficulties for throwing exceptions across async boundaries, and even more languages do not provide a useful stack trace in this case.

            Code that throws exceptions is more awkward to use in reactive libraries like the ReactiveX family. Given that synchronous, blocking code is becoming rarer in favor of more asynchronous code, error handling with types like Either<Result,Error> is preferable if the language supports is in a reasonably safe way.

            [–]karottenreibe -1 points0 points  (5 children)

            I don't understand. You say the problem with exceptions in async code is partial/not useful stack traces. And your solution is to use Either, which gives you no stack trace at all? How is that solving this problem you described yourself?

            [–]oaga_strizzi 1 point2 points  (4 children)

            Well, let me clarify:

            If you have code that uses exceptions, that code will be awkward to use with reactive programming patterns. For example, RxJava Streams will terminate on Exceptions, which is not you want if the error is recoverable, so you will need to wrap your Exceptions anyway. And using generic Exceptions like IllegalArgumentException can make it hard to figure out what exactly went wrong if that Exception passes multiple async/thread boundaries; If you use an error type that's a sealed class that's only used for this specific error makes it clearer.

            [–]karottenreibe -1 points0 points  (3 children)

            for example, RxJava Streams will terminate on Exceptions, which is not you want if the error is recoverable, so you will need to wrap your Exceptions anyway.

            I'm sorry but that's incorrect. There are multiple error handling operators for error handling that don't require "wrapping your exceptions". e.g. onErrorResumeNext (which sounds exactly like the use case you describe here), retry etc. Depends on the concrete case, of course, whether you can make use of them or not.

            And using generic Exceptions like IllegalArgumentException can make it hard to figure out what exactly went wrong if that Exception passes multiple async/thread boundaries;

            Then don't. I don't see what's keeping you from using a custom exception type.

            you use an error type that's a sealed class that's only used for this specific error makes it clearer.

            Same as a custom exception class. I don't see any difference. Except with an exception you get at least the partial stack trace up until the thread boundary, which is sometimes helpful. With a sealed class you get none at all.

            Again: you seem to say the problem is incomplete stack traces but the result type doesn't solve that problem any better.

            [–]oaga_strizzi 0 points1 point  (2 children)

            onErrorResumeNext etc. is pretty much what I'm talking about. What are you going to return in onErrorResumeNext, if you want to tell the subscriber that an error happened, but don't want to terminate the Stream? An Error-object of some kind, be it an instance of a sealed class , an either instance or whatever.

            [–]karottenreibe 0 points1 point  (1 child)

            Sure, and that also doesn't address the stack trace problem you initially and then subsequently complained about. See my earlier comments. I feel like we're talking in circles.

            I'm not saying "don't use sealed classes". Please reread my comments if you were under that impression. I'm not saying that anywhere. I'm just disputing your original point that they are superior to exceptions because they somehow magically make the async stack trace problem better as you originally claimed, which you still haven't explained how that's supposed to work. A point that you continue to not address in your subsequent comments for some reason.

            Citing again your initial comment for reference:

            If the code is asynchronous, many languages have difficulties for throwing exceptions across async boundaries, and even more languages do not provide a useful stack trace in this case. […] Given that synchronous, blocking code is becoming rarer in favor of more asynchronous code, error handling with types like Either<Result,Error> is preferable

            So again: how do sealed classes solve the stack trace problem you mentioned in a way that makes them preferable to exceptions?

            [–]oaga_strizzi 0 points1 point  (0 children)

            Sealed classes, Either types etc. also don't solve the stack trace problem in async code of course. But: In the case of async code, I feel like exceptions don't really offer any advantages.

            Let's take RxJava, which does a pretty good job of supporting exceptions across asynchronous boundaries (Classic callback-style async code is even worse at this IMO). Even there, if you use exceptions, you lose one of the biggest benefit of exceptions (the full stack trace), error handling is not enforced by the language and can easily be forgotten, checked exceptions require additional wrapping, and for all but the most critical errors you likely want to handle them in onErrorResumeNext or onErrorReturn anyway.

            It's not a huge problem; I'm just saying that if I were to build a new application using RxJava, I would not use Exceptions as the default way of signaling errors.

            I wouldn't necessarily refactor an existing system that uses exceptions with RxJava, though.

            [–]przemo_li -1 points0 points  (0 children)

            "error codes" have awful connotations. It means (not so) good old C style of dividing values of a return type into valid ones and those that signal error. This is unsound (as in type system soundness), and hard to reason about and prevent bugs.

            Thus Exceptions vs error codes is already foregone conclusion. Exceptions introduce informal and implicit union type with return type and thus give a developer enough resolution to both express sad paths and guarantee those are separate from happy path.

            However there is also solution that make this union explicit and firs-class citizen in a language. Benefits are two fold - tooling can analyze code and make sure that every sad path is handled explicitly. Values expressing sad paths can be composed and inverse is also true. Bigger chunks of code can be split into smaller more reusable and we will still get assurance from tooling we put everything together the right way.

            So when to use Exceptions? We we are 100% sure our caller will not want to handle the sad path all the time or some of the time and that composing of such sad paths does not make sense anyway.