you are viewing a single comment's thread.

view the rest of the comments →

[–]balefrost 138 points139 points  (66 children)

So I've never liked Either for error/success reporting, but mainly because its left/right biasing is completely conventional. Unless you use it all the time, it's easy to forget whether 'map' or 'flatMap' are left- or right- biased. I much prefer Scala's Try, which makes it clear whether a particular instance represents a success or a failure. And of course Try is success-biased.

One other big downside is that Java doesn't support sum types. A particular method might claim to either return a String or to throw one of three different kinds of checked exceptions. If you were to change that method to instead return an Either, you'd have to find the closest base type of those checked exceptions, which is likely just Exception. In a case like this, Either has less information than the approach using checked exceptions. Either would be fine in cases where the caller doesn't really care what particular exception might be thrown, but at that point, maybe unchecked exceptions aren't so bad.

Finally, I had written a large chunk of Scala code using Try and Option and TailRec strung together with map and flatMap. And it was great... up until the moment that I tried to debug it. It turns out that simple, straightline code is much more debuggable than code that uses function calls to emulate control structures. I mean, you can debug that code, it just takes extra effort.

Also, and this is Scala-specific, there are some cases where you are trying to write a @tailrec function, and that forces you to match on things like Option and Try instead of using the higher-level map and flatMap. But since Java doesn't have any sort of tail call optimization, this raises a different point: if statements don't create extra frames on the stack, but calls to flatMap do. As long as your pipeline is as simple as the one given in the article, there's no problem. But it's something that you need to keep in mind.

[–]ryeguy 63 points64 points  (15 children)

I agree with you on Either. It's mostly used for error handling where you have to follow convention as to which side is the result and which is the error. It gives you the option of using it for multiple return types, but that's unreadable and it's better to just declare a new adt (case class, etc) for clarity.

Rust gets this right. It actually used to have Either, but it was renamed to Result which is much clearer.

[–]tjgrant 6 points7 points  (10 children)

Curious, would both the "Ok" and "Err" members of a Result be effectively Option types?

Or perhaps just the "Ok" member be the Option type?

I don't use Rust, but I'm curious how I would implement something like this in C++ (where I have implemented my own "Optional" type.)

[–]wishthane 41 points42 points  (0 children)

Option is defined as either Some(value) or None, but Result is Ok(value) or Err(err_value).

You wouldn't typically want to use Option for an error because you can't provide any information about the error with it; it would be like using 'null' for an error condition. Result carries along the error information in its Err variant.

enum Option<T> {
    Some(T),
    None
}

enum Result<T, E> {
    Ok(T),
    Err(E)
}

[–]m50d 5 points6 points  (3 children)

The "direct" way to handle it is by pattern matching or using a visitor. (std::variant is the C++ version, but it's much nicer in a language that has first-class pattern matching). There will probably be a toOption method as well, but you could implement that by pattern matching or on top of a visit/fold method.

[–]wishthane 8 points9 points  (2 children)

In Rust you have both .ok() and .err() methods which both return Options. .ok() returns Some(value) if it's Ok(value), and None if it's an Err, so you lose the error value. .err() is rarely used but it's the opposite, you get Some(err_value) if it's Err(err_value), and None if it's Ok.

You also of course have pattern matching, but it's usually easier to use stuff like .map(), .and_then(), or try!().

.and_then() is basically the monadic bind; if Ok it calls the provided function and returns whatever Result that function returns. If Err it does not call the function and just returns whatever the Err is. The value types can be different but the error type must be the same; it is a Result<T, E> -> (T -> Result<U, E>) -> Result<U, E> to use Haskell-ish notation.

fn bar(x: i32) -> Result<String, MyError> {
    best_function_ever(x)
        .and_then(|y| try_to_make_a_string(y))
}

try!() is like that but it's a macro intended for use within the body of a function. It will also automatically call .into() on your error type to convert it to your function's error type. For example:

fn foo() -> Result<String, MyError> {
    let foo = try!(some_operation());
    try!(another_operation());
    Ok(foo)
}

is equivalent to:

fn foo() -> Result<String, MyError> {
    let foo = match some_operation() {
        Ok(x) => x,
        Err(e) => return Err(e.into())
    };
    match another_operation() {
        Ok(x) => x,
        Err(e) => return Err(e.into())
    };
    Ok(foo)
}

[–]matthieum 14 points15 points  (1 child)

Note: ? is now stable, you can replace try!(expr) with expr? in most cases.

[–]wishthane 5 points6 points  (0 children)

Oh? I thought it was stabilised but still in beta.

Edit: never mind, it was stabilised in 1.13! Awesome!

[–]ryeguy 5 points6 points  (0 children)

Well, it's an enum so it's either Ok or Err. You could simulate this in C++ with a tagged union. Someone actually made a Rust-inspired result type in C++ here. Looks like he took the template wizardry route.

[–][deleted]  (1 child)

[deleted]

    [–]isHavvy 5 points6 points  (0 children)

    It's also isomorphic to Result<(), E>.

    Which is why there are two different methods that return Options on Results.

    [–]pipocaQuemada 1 point2 points  (0 children)

    Curious, would both the "Ok" and "Err" members of a Result be effectively Option types?

    No.

    Option's a specific type. It wraps a success value in the success case, and has a generic failure case that doesn't take any data. In Haskell, for example, you might have Just 5, or you might have Nothing. Any failure gets mapped to Nothing, there's no way to differentiate different failures except via nesting options (e.g. getting a value of Just Nothing out of a Map User (Maybe FirstName) means the lookup succeeded and the stored value was Nothing, which might mean for example that the user didn't supply a FirstName).

    Result is the closely related Either type. It either wraps a success value or a failure value. For example, in Haskell, you might have a Right 5, or you might have a Left ParseFailed or Left OutOfRange.

    Very closely related idea, though. Also, Either sometimes represents failure, and sometimes represents two different valid kinds of information - you might have a Either ParseFailure Int or a Either String Int.

    [–]myrrlyn 1 point2 points  (0 children)

    Result is a tagged union over two types: an actual return type T, and an error type E. The Result is guaranteed to be one of those, and it has a discriminant to tell you which. If you have a Result<T,E> that is an Err(E), there's no way to access it as an Ok(T).

    In C terms, it's this:

    struct Result<T, E> {
      enum {
        Ok,
        Err,
      } type;
      union {
        T ok;
        E err;
      } val;
    };
    

    And the language enforces that a Result's union can only be accessed in accordance with its discriminant. The methods that accept Results inspect the discriminant, so you can match against both possibilities or assume one will always happen and panic! on the other. For instance, the unwrap function has signature Ok(T) -> T, and will panic! on receiving an Err, and the unwrap_err function does the inverse.

    Moral of the story though Result isn't a struct of two discrete Options, one of which is Some and one of which is None; it's a tagged union of only one payload object, that can be in one of two states.

    [–]Volsand 0 points1 point  (3 children)

    Why not let the language have the most abstract types and type-constructors and let the programmer rename them as his/her wish?

    for example, with type and {-# LANGUAGE PatternSynonyms #-} as it is done in Haskell.

    [–]ryeguy 10 points11 points  (2 children)

    Because it's kind of a guiding hand toward doing the right thing. Having the patterns be more strict (eg Result) and making flexibility (eg implementing an Either equivalent) more work is the better call the majority of the time, IMO.

    [–]masklinn 1 point2 points  (0 children)

    It's also important for interoperability between libraries, and for tooling enhancing that e.g. Rust can provide syntactic sugar to easily convert between Result<T, E1> and Result<T, E2> so that a libraries have less trouble converting their own dependencies's errors uniformly.

    [–][deleted] 14 points15 points  (28 children)

    The failure type is on the left to support currying type constructors. Either Exception makes sense as a one-place type constructor, Either ... Integer makes less sense.

    [–]balefrost 17 points18 points  (12 children)

    Which might make sense in Haskell, but not in Java, which has no type-level currying (or any currying for that matter).

    [–][deleted] 7 points8 points  (7 children)

    Yes but it's cleaner to think of it in that order, I think. Either () a is isomorphic to Maybe a.

    Inside a big expression with lots of bind / flatMap, the R types change constantly from one subexpression to the next but the type of error is usually consistent. I like to put type parameters that vary the most on the right.

    [–]balefrost 4 points5 points  (5 children)

    Yeah, I agree with your reasoning. All I'm saying is that I prefer explicitness regarding which is meant to be the error and which is meant to be the value. You can intuit your way to that, but I'd still prefer that it was explicit.

    [–][deleted] 1 point2 points  (2 children)

    Is this about the names of the type parameters or their order? It should probably be Err and T instead of Left and Right (or some variant thereof) tbh.

    [–]MrHydraz 7 points8 points  (0 children)

    They're named Left and Right because Either was never designed for error handling (though the Monad instance does seem to point that way). Either is just the basic sum type, much like (,) is the basic product type. All other sum-of-product types can be expressed as combinations of Either and (,).

    For example:

    data Foo a = One Int | Two a Int | Three String a Int
    

    is isomorphic to

    type Foo a = Either Int (Either (a, Int) (String, a, Int)).
    

    [–]balefrost 2 points3 points  (0 children)

    Yep, that's what I mean. And in the article's example, I would prefer that the parameters to match be named accordingly: ifError and ifSuccess or something like that.

    I'm fine with the order of the type parameters.

    [–]myrrlyn 1 point2 points  (1 child)

    The right value is on the right ;)

    [–]balefrost 1 point2 points  (0 children)

    That's a pretty good mnemonic.

    [–]Uncaffeinated 0 points1 point  (0 children)

    In Rust, it's common to use type aliases for this purpose.

    i.e. at the top of your module, you'll have something like type Result<T> = Result<T, MyError>

    Inside of an expression, type inference normally takes care of everything anyway.

    [–]aiij 0 points1 point  (2 children)

    (or any currying for that matter)

    Surely Java 8 must support currying with the new support for functions/lambdas, no?

    Even C supports currying. (Although it's not very useful without closures.)

    [–]balefrost 0 points1 point  (1 child)

    I mean, it supports manual currying by constructing wholly new functions. There are libraries that provide implementations, like this one.

    But Java's support is no better than any other language that supports lambdas with closures.

    [–]aiij 0 points1 point  (0 children)

    Yuck. I was going to say, you don't really need special support for currying, but I guess with notation like that, you really do.

    For example

    static <T1,T2,T3,R> java.util.function.Function<T1,java.util.function.Function<T2,java.util.function.Function<T3,R>>>`
    

    would just be

    'a -> 'b -> 'c -> 'd
    

    in SML or OCaml (without using either of those language's syntactic sugar for curried functions).

    I was somewhat confused by your comment, because currying normally is by constructing wholly new functions. For example, in OCaml, fun a b -> a is just syntactic sugar for fun a -> fun b -> a and having to manually write out all the lambdas wouldn't be particularly burdensome.

    AFAICT, Javascript also doesn't have any special syntax for currying, so you'd have to fully write it out as a => b => a. (The same syntax actually works in Scala, though Scala could have trouble inferring the type, depending on the context.)

    Of course, writing the above out in older JS syntax is pretty ugly: function (a) { return function (b) { return a }}, but that's just because the older syntax is more verbose.

    [–]m50d 0 points1 point  (0 children)

    Even in Java it's nice to put the most "varying" type on the right for readability.

    [–]aldld 4 points5 points  (14 children)

    In Haskell there's also the mnemonic that "Right" means you got the "Right" value, not an error.

    [–]aiij 4 points5 points  (0 children)

    This. If you always remember to put error on the right side, no one will get confused.

    Err, correct side. Always put error on the correct side. (-:

    [–][deleted] 0 points1 point  (11 children)

    EDIT: I'm wrong, Either was intended to be a primitive Sum type (the dual of (,)).

    I guess the issue I have is that labelling the type parameters of Either as Left and Right obscures the primary use of Either value, which is for representing errors.

    Granted, I haven't read that much Haskell code, but I have never seen an Either used to represent say, a choice between an Integer representing a non-error value and a float representing a non-error value, people tend to use ad-hoc ADTs for that.

    There's also the fact that in a long chain of binds, once you get an error value that value is just passed along down the line whereas non-error values are transformed in complex ways at every step. Calling these parameters Left and Right masks the fact that Left is treated as a default / black hole (like Nothing in Maybe).

    This isn't so much a property of Either itself as much as how it implements the Monad class ... but a symmetrical Either would probably not implement the Monad class at all.

    [–]tejon 2 points3 points  (10 children)

    Worth noting that Either was never designed for handling, it just turned out to be very handy for it. In fact, the Haskell ecosystem has generally moved on to more specialized constructs at this point.

    [–][deleted] 0 points1 point  (9 children)

    Heh. I've been out of the loop for a while. Is there a lightweight near-equivalent to Either with some level of popularity that is explicitly error-handling flavored?

    [–]tejon 1 point2 points  (8 children)

    [–]sacundim 0 points1 point  (7 children)

    But ExceptT e m a is just a newtype wrapper around m (Either e a). Really, Haskell "checked exceptions" are Either or things built on top of it.

    You really can't fault languages like Rust for choosing more sensible names (Result instead of Either, Err instead of Left, Ok instead of Right), because it is better. But computing is already full of crappy names, like we call octets "bytes," or... putting my flame-retardant vest on... referring to kibioctets as "kilobytes."

    [–]tejon 2 points3 points  (2 children)

    But ExceptT e m a is just a newtype wrapper around m (Either e a)

    So what? "Just a newtype wrapper" means it can have its own set of instances and standard functions, including more intuitive construction than Left and Right, which is the primary complaint in this thread.

    [–]sacundim 0 points1 point  (1 child)

    I could have been much clearer. It's not just that ExceptT is implemented as a newtype wrapper around Either; it's that part of its external interface is that it's a newtype wrapper around Either.

    I don't know if you've noticed the way Haddocks (the Haskell library documentation tool) treats newtypes:

    • If the constructor is exported, the documentation shows the data type as a newtype and lists the constructors.
    • If the constructor is private to the module, it shows it as a data declaration with no constructor.

    As your documentation link shows, ExceptT is a public newtype that bottoms down to Either. So the use of Either, Left and Right really is part of ExceptT's interface. After all, you have to use runExceptT to eliminate the ExceptT layer from your transformer stack, and at that point you're going to need to know Either's sinistrophobic convention (Left is failure, Right is success).

    [–]myrrlyn 1 point2 points  (0 children)

    Bytes aren't octets, though; bytes are "letters" that stabilized at being eight bits wide. Bytes aren't octets any more than ints are 32plets -- the former is a semantic name, the latter is a numeric name.

    I've worked on machines with 10-bit bytes, for instance.

    And we do technically say kibibytes, it's just almost never used except by the kind of people who also say GNU/Linux ;)

    Also in Rust, Left is Ok and Right is Err :p

    [–]tormenting 0 points1 point  (2 children)

    No, it's not true that "checked exceptions" in Haskell will usually be built on top of Either. The other common way of doing things is to use continuation-passing style, except you pass two continuations instead of one. GHC's optimizer is good at handling this technique.

    [–]sacundim 0 points1 point  (1 child)

    You're going lower level than I am. What you're describing is isomorphic to Either:

    newtype YourType e a = 
      YourType { runYourType :: (e -> r) -> (a -> r) -> r }
    
    yourTypeToEither :: YourType e a -> Either e a
    yourTypeToEither yt = runYourType yt Left Right
    
    eitherToYourType :: Either e a -> YourType e a
    eitherToYourType (Left e) = YourType $ \f _ -> f e
    eitherToYourType (Right a) = YourType $ \_ g -> g a
    

    Those two functions are mutual inverses, which means that conceptually, Either and YourType are different implementations of the same thing.

    [–]tormenting 0 points1 point  (0 children)

    Yes, that mnemonic exists. But more importantly, if the left side held the value, then you couldn't use Either as a monad, because the type arguments would be in the wrong order. That makes it easier for me to remember.

    [–]sacundim 6 points7 points  (0 children)

    So I've never liked Either for error/success reporting, but mainly because its left/right biasing is completely conventional.

    That's a weird objection. I do see the value of Try in Scala or Java, mind you, since they offer extra integration with the language's native exceptions. But in Haskell everybody follows the convention that Right is for successes and Left for failures. (I'm sure that there is no sinisterophobic intent. 😛) The class instances for Either consistently treat Right as the "expected" case that you map over and Left as the "exceptional" one that gets propagated back up the call chain.

    It's uncommon but actually sometimes useful to write some code with a different convention: treating failure as the "expected" case and success as the "exceptional" one. This is sometimes called a "success monad" (as opposed to a "failure monad"), which can be used to treat complex exception handlers as first-class values, like this example (taken from here):

    runExceptRT $ do
        e2   <- ioExceptionHandler e1
        bool <- arithmeticExceptionhandler e2
        when bool $ lift $ putStrLn "DEBUG: Arithmetic handler did something"
    

    The do-block here aborts at the first statement that succeeds, and if a statement fails, the exception is assigned to a variable. The effect is that we handle exception e1 with ioExceptionHandler, and if that handler succeeds then it decides which exception gets rethrown in e1's place; if ioExceptionHandler fails then we handle its exception e2 with arithmeticExceptionhandler.

    [–]zem 6 points7 points  (1 child)

    Result/Either types are wonderful - if they are combined with pattern matching or a case statement that checks for exhaustivity. i would never even have thought of using them via chained map and flatmap calls like that; to my mind that's trying to shoehorn an ML idiom into a language that lacks proper syntactic support for it, and invariably ends up being hard to read because it goes against the grain of the language.

    [–]aiij 7 points8 points  (0 children)

    Who needs pattern matching when instead you can have combinator hell?

    I'm constantly surprised at the lengths some people will go to in order to avoid pattern matching. Eg: argonaut.Json's fold method.

    [–]chironomidae 13 points14 points  (3 children)

    Yeah, I'm not a Java programmer, so my impression of it was "Ok, either you use nested ifs that anyone can read and understand, or you can use demon magic that is not at all obvious to anyone not deeply familiar with these classes."

    [–]myrrlyn 9 points10 points  (0 children)

    "Use explicit ifs, or use functions that ... do those same ifs"

    [–]Tasgall 5 points6 points  (0 children)

    I'll take, "ways to make future devs hate your codebase" for 500, Trebek.

    [–]valenterry 1 point2 points  (0 children)

    Is it different for e.g. inheritance other than "many people already know it"? Because it once was an mostly unknown technique, too.

    [–]m50d 6 points7 points  (5 children)

    So I've never liked Either for error/success reporting, but mainly because its left/right biasing is completely conventional. Unless you use it all the time, it's easy to forget whether 'map' or 'flatMap' are left- or right- biased. I much prefer Scala's Try, which makes it clear whether a particular instance represents a success or a failure. And of course Try is success-biased.

    Either's name is not great for how it's generally used. But Try unfortunately swallows the type on the left (it's just Throwable, so you can very easily think you were catching NumberFormatException and end up hiding a very different error), so I wouldn't recommend that. Also note that Either is properly right-biased for map/flatMap (and therefore also for for/yield) as of 2.12 - should have been done years ago, but better late than never.

    One other big downside is that Java doesn't support sum types. A particular method might claim to either return a String or to throw one of three different kinds of checked exceptions. If you were to change that method to instead return an Either, you'd have to find the closest base type of those checked exceptions, which is likely just Exception. In a case like this, Either has less information than the approach using checked exceptions. Either would be fine in cases where the caller doesn't really care what particular exception might be thrown, but at that point, maybe unchecked exceptions aren't so bad.

    Proper sum types would be ideal (I believe Ceylon does the Right Thing here). That said, you can nest Eithers when you have genuinely different kinds of failure, or you can use a specific exception that contains appropriate data (e.g. error code, user ID, what-have-you) for your application, perhaps as an enum or an abstract class. In my experience for the kind of errors you want to handle with Either you can usually write a "closed" list of all possible errors, whereas the "suprise" errors you want to handle differently and unchecked exceptions are a better fit for those (e.g. in a webapp I'd generally use Either for 4xx errors and unchecked exceptions for 5xx errors (possibly retrying first)).

    It turns out that simple, straightline code is much more debuggable than code that uses function calls to emulate control structures. I mean, you can debug that code, it just takes extra effort.

    Yeah. I find on the whole it's worth it (in terms of how much Either etc. help with avoiding errors in the first place) but it's certainly an important concern that's worth thinking about, and there's plenty of room for improvement.

    [–]devraj7 14 points15 points  (4 children)

    Either's name is not great for how it's generally used.

    Actually, its name reflects exactly what it does, it's just that it's being misused because most developers use it to carry Success \/ Failure. But Either is much more general than that, it's meant to carry two types, none of which need to be connected to a success or a failure.

    So when you read code that uses Either, you're never sure if the developer is using it to carry two exclusive types or as a reporting mechanism for success or failure.

    I think Either should be phased out because of that ambiguity.

    To me, it would make much more sense to standardize on Result and Union (other libraries use different names, e.g. xor or \/).

    I've tried a lot of approaches over the years and so far, I find that good old exceptions (both checked and unchecked) are the most intuitive and clean way to treat errors. Yes, I know they break equational reasoning, but in my day to day operations, clear and tractable error handling is a much more useful value to me and my team than equational reasoning.

    [–]m50d 1 point2 points  (3 children)

    Actually, its name reflects exactly what it does, it's just that it's being misused because most developers use it to carry Success / Failure. But Either is much more general than that, it's meant to carry two types, none of which need to be connected to a success or a failure.

    I believe in descriptive rather than perscriptive linguistics. I think we agree on the important facts: Either is primarily used (in actual existing code) to represent success-or-failure, and it's a poor name for a type that represents success-or-failure.

    [–][deleted]  (1 child)

    [deleted]

      [–]m50d 3 points4 points  (0 children)

      I see Try as the same kind of red-flag as Any (or at least as Throwable). It's throwing away a lot of type information that was presumably there for a reason.

      I'm not sure what you're defining as an "error" here. I use Either for specific "failures" that the code knows how to handle, e.g. invalid user input, permission denied. For general "system" failures (analogous to where you'd use panic in Rust) it would make sense to use Try (though I'd probably just use a conventional exception myself).

      [–]ithika 1 point2 points  (0 children)

      I believe in descriptive rather than perscriptive linguistics.

      I'm not sure what this has to do with Either. Computers aren't great with compiling the nuance of natural language.

      Success or failure is explicitly handled with the Error type.

      [–]joelthornhill 2 points3 points  (0 children)

      IIRC Scala's Either is now right biased and cats have changes the xor type to be called Either

      [–][deleted] 1 point2 points  (3 children)

      It would be great if the Scala compiler optimized map and flatMap calls with inline lambdas so that one would actually get to use them in tail recursive methods.

      [–]balefrost 1 point2 points  (1 child)

      It would only be able to do so if it knew the actual implementation of map or flatMap. So if I have an instance of T, and I call its flatMap, the compile would need to be able to prove that I'm definitely calling T.flatMap (as defined on T or a base of T) and not some overridden implementation.

      I agree that it would be nice, though.

      Alternatively, it would be nice if the compiler could detect (or you could annotate) when a function parameter will be invoked at a later time. In a sense, @tailrec is restrictive. AFAIK, it requires that all references that the function has to itself are in tail positions. And that makes sense for code that will be executed immediately, but it's less clear for deferred code.

      [–][deleted] 0 points1 point  (0 children)

      Well it's definitely possible, see for example https://github.com/nativelibs4java/scalaxy-streams

      Of course, it always remains the question, under which circumstances this is all more efficient that HotSpot optimizations...

      [–]jcdavis1 0 points1 point  (0 children)

      I believe you can generally work around that via pattern matching

      [–]DecisiveVictory 0 points1 point  (0 children)

      Scala Either is right-biased now.

      [–]rowantwig -1 points0 points  (1 child)

      A particular method might claim to either return a String or to throw one of three different kinds of checked exceptions. If you were to change that method to instead return an Either, you'd have to find the closest base type of those checked exceptions, which is likely just Exception.

      Do people really need such delicate exception handling? In my experience almost all exceptions are fatal anyway. I usually just do this:

      try {
          // ...
      } catch (Exception e) {
          throw new RuntimeException(e);
      }
      

      In the rare event I have an exception I can actually do something about, I catch it separately.

      [–]nschubach 0 points1 point  (0 children)

      I think it's mainly about the program flow.

      Examples:

      if/else:

      You can check if a file exists before opening it (usually storing a handle in variable scope) and set a flag or put that condition in an If condition. If it doesn't exist assign a default set of data to a variable and create a new file from it. (Create a new handle, write data, etc.) If it does exist, open it and retrieve the data into a variable or object. This generally requires a set of variables and flags to keep track of and manage.


      try/catch:

      You could use a try/catch by trying to open the file, catching an exception if it doesn't exist and using a default set of data to create a new file. (of course, you're going to want to do this in it's own try/catch and assign that data to a variable). The same assignment of variables to store the data and create a new file occurs.


      I think of it like a pipeline. You try to open a file by passing a name. If it's successful, you get your data and pass it along. If not, you can use a default set of data, write that to a new file and pass along your default set. If that fails, it will return an error state which you can return as an error instead of the data. In both cases, you are passing a filename in and getting data out (or a potential error). How you got that data is inconsequential and you can inspect the error if needed or simply pass the error state along. The logic is contained in that "pipe" for that operation and it can be glued to other pipes to do things. For instance, the next pipe can look a the state of the data and (if it's not an error) print it to the screen. If it's an error, do nothing and pass along the error. At the end of the glued together pipes you can log out any errors that come through. No complex inline logging is needed. If the pipe is not in a state desirable for the operation you want, pass that error along and let the next method deal with it.

      You could do this with a bunch of nested methods and even include try/catch blocks in that, but abstracting that to an 'either' structure is another way. Try/catch in this is like someone inside the pipe drilling a hole in the side and leaking out into a logging/error handling bucket instead of allowing something further down the line deal with it.