Avoiding Nested Errors
How can you avoid deeply nested error handling code? This week we review Gabriel Gonzalez’s trick for keeping error handling flat.
Episode 45 was published on 2021-05-10.
>> Hello and welcome to the Haskell Weekly podcast. This is a show about Haskell, a purely functional programming language. I'm your host Taylor Fausak. I'm the Director of Software Engineering at ACI Learning. With me today is Cameron Gera, one of the engineers on my team. Thanks for joining me today, Cam.
>> Thanks for having me, Taylor. It's been a minute since we got the podcast. So I'm excited to be back today. Have a good little chat and just spend some quality time together. You know, it's good stuff.
>> Yeah, it has been awhile. I'm looking forward to it today. What are we going to be talking about?
>> Well, yeah, we're going to be talking about something that's the unnamed, known way of avoiding deeply nested error handling code. So if you're familiar with Haskell at all, you've probably written a function or multiple functions that have two, three, four layers of nestedness just to get the correct error message tossed up through the top. And so. You know, this blog post by Gabriel Gonzalez is talking about a way to take advantage of either to, you know, avoid some of that as well as do notation. So, you know, I'm gonna shoot my best shot here and try to name it. There's two names I have. So we have the blah, excuse me, advantageous. Yeah. Wow. I can't speak today. Advantageous do notation as one or erroneous do notation as the other. So, you know, for those on Reddit, Twitter: Yo, vote.
>> We'll start a poll.
>> See who wants to, who wants to take that on? Um, yeah, just a thought I could be wrong too, but, uh, yeah, we're going to just talk more about that today. Um, which is something we've come across ourselves.
>> Yeah, I wanted to talk about this one today, because like you mentioned, this is something we come across in our code base frequently. And it's been a difficult refactoring to suggest because there isn't a name for it. You can't say, Oh, you should, extract a function here or, you know, whatever. And for the benefit of our listeners, the pattern we're talking about, which you touched on already, is this increasing indentation when you're handling different cases. So very frequently for us, that happens when you're casing on a maybe or an either. And in one of the two branches, all you want to do is throw an exception and basically stop reporting at that point. Or stop moving forward. So if you'll do like, case some maybe of; nothing, throw some error; just some value, carry on with that value. And if you do that a couple of times, you get really nested. And I'm saying "throw an exception." For us, in our application that we work on, we're in a monad where we can throw exceptions, but the same principle can be applied to eithers or maybes or any number of things. So it doesn't necessarily involve exceptions.
>> Right, right. And you know, there's nothing against super nested case statements. Like that's not what we're getting against here. We're just trying to say, there's some other way. There's a better way for readability and maintainability long-term . So that's the only reason we're recommending this. And this is a good pattern, I think for Haskell developers to grow in, because it can get really frustrating and confusing when you're trying to trace down the exact, what's happening at each case. And so, you know, he's, he's of kind of lean into the, either in this blog post, and we'll kind of probably piggyback off of that as well as relate it back to what we do on a day to day. But the big idea is the do notation allows you to short circuit more or less when you give it a, an error case or an exception case to some degree. Um, and so what he uses is the ability to, uh, throw that as he's trying to validate a string, that it should be an age, um, and should they be alive or not alive? Um, So that is his kind of leading motivating example here. And you know, his first one is nested to the third degree. I would say a fourth degree. Like there's quite a few there's we'll just say there's three layers of left you can get to. And so what he tries to do is extract that out and say, all right, if we're going to be not be able to read. The age, we just need to say here's, it's an invalidated. Just don't even try anymore of the code. Uh, you know, and then so much, you know, same with the Boolean values. Is it alive or not alive? So there's really, honestly it brings the code down to like three separate login logic steps. Right. You want to first check? Is that the age a valid string and is their age. Greater than zero. And then are they alive? And like, that's, you know, the three logical concepts that need to be part of this validation. So I think it separates it really nice. Um,
>> Yeah, that's one of the things that I think this, uh, this is very explicit, but, um, or this explicit form of pattern matching on of these values is clear when you're stepping through it, but it also kind of muddles the overall structure of what you're doing in the function, because you have to each time there's a case you have to like. Figure out and keep in mind. Okay. There's two or more branches that I have to deal with here. So when I'm reading the code, I have to keep that in mind. And I don't know if I read one branch first and it's really long. I still have to, when I get to the next one, kind of like pop my mental stack and go back to the next one or go back. And that clearly it gets harder. The more cases are nested with each other. Cause you have to keep more on that mental stack, ready to pop off. Um, and so that's like my, um, that's pragmatically. I think why I like this uh refactoring technique to remove this layer of nesting, but also, uh, just kind of aesthetically code that increasingly indents is a lot more difficult to read because you have, if you keep to a particular line length, whether that's 80 or 120 or whatever, um, you know, you're eating into that line length budget. As you make more of these cases. And some people may argue that that's a good thing. You shouldn't have this many nested cases. And that is your programming language pushing back on you and saying, you know, break these out into separate functions, do something different. Um, but in this case, usually it's pretty straightforward. Like if you had a flow chart at each point, you'd say, if something goes wrong, you know, go to this error state. And I think that is captured in this pattern very well that he describes here.
>> Yeah, I think it was, yeah. It's something for us. We've, you know, had code go through code review and we're like, Oh yeah. Let's, you know, this is a lot of nestedness. How can we resolve this? Uh, and so with time, you know, we we've started to have the mental note of, Oh, let's just choose to take advantage of the monad we're in with the do notation and short circuit, because we don't have to necessarily worry about. Oh, what would happen even if there was a success or whatever, because without success, we can't go any further, more or less. Um,
>> And that, uh, that short circuiting behavior that you're describing is one of the like trademark, uh, features of Haskell of do notation is you can, I think it's often billed as the programmable semi-colon. So like, you get to decide what happens for each line and your do notation and for. Maybe, and either when those are used in do notation, the behavior is short circuit on nothing or left respectively.
>> Right. And for us, we've, you know, worked on short-circuiting in, uh, with maybes, but adding, turning that maybe into an either with some sort of error message so we can, uh, you know, Have some sort of information of what happened or what went wrong. And so, you know, we've created our, our functions name, note, um, just takes a string and a maybe value. And we'll either give you that value or give you a left or give you a right that value or a left. Um, and so, you know, that's very similar to his, uh, or die function. I believe that he mentions in here,
>> Yeah it's a good way to like decorate extra information, because I think for us, a lot of the times we use functions that return. A maybe because for that particular function, there's only one obvious way that it can fail. So there's no sense in having it return, you know, a descriptive message. So as an example, like if you want to parse. A string into a number. If you can't do that, then really the only reason why is that it's not a valid number. There's not many other cases that go on, but if you want to parse it, parse a string into a number, and then also validate that that number, meet some other criteria. Suddenly you need to note hint hint something additional about that, uh, error case. So that's where we use note to turn a, maybe into an either.
>> Yeah, and I think it's been really helpful, um, as we move forward, but we've talked a lot about case expressions here with, you know, just, or nothing turning those into eithers there's also the, uh, some helpful functions that allow you to short circuit on, uh, a sort of particular failure case in a monadic way. So, um, there's. Guard, when, unless that allow you to give it some predicate. And if that predicate fails, it will do this thing, which if it's short circuit left and that's what it will do, and it will exit out of your function. So. Um, you know, there's also those options. Uh, there was, you know, in, in the example, I think he's already gotten some comments on Reddit about it as well. Um, just of using an if to return, you know, if it was true, it would. Return on a failure, but if not, it would return uh just pure, or return unit. Um, which isn't the worst thing in the world. But, um, there are other options too, that allow you to short circuit, maybe in a little cleaner fashion.
>> One of the things that I really like about Haskell and refactoring in Haskell is that you can often do, like when you do one refactoring, another one becomes available. So, and this isn't just true in Haskell, but it works very well in Haskell. So in this case, if you refactored one of these nested expressions to something that says. If some condition, then you throw an error or return left or return nothing else. pure unit, that thing is really easy to change into. Well, instead use the helper function when and say, when that predicate throw the error and then there's no else case it's not, you don't have to think about it. And then if your condition is the other way around, if it's inverted, you can say, unless some condition throw some error. Uh, for us, we have found that. When is a lot nicer to use than unless on our team. It's, it's pretty much use when, unless you have a good reason not to.
>> Oh, I see what you did there with, unless yeah, I think it's just. When it, when it flips to the logic and the predicate, it gets confusing to use unless, so we've opted generally to use when not, or when not equal to, rather than unless equal to or unless the value.
>> Yeah. And the other one that you mentioned guard is a helper function. I think it's not built on top of monads directly, but uses a related, uh, type class called monad plus and monad plus has the concept of something called mzero. And this is usually thought of as like the monadic version of a monoid. So you have mzero and you have mplus which behave very similarly to. mempty and mappend, um, and mzero often is implemented as fail. And so this all sounds kind of crazy, but it's often used to implement parsers where you say, try this or try this and, or, and then you can connect them together using mplus, and then you're like fall back case is mzero.
>> And, and I mention all that because guard has the downside of, if you use it to check in one direction and then throw an error, if the guard fails or the error you're going to get is usually useless because mzero doesn't carry around any particular information. It just means, Hey, something went wrong. Um, which means you might have to flip the conditional in your guard so that it succeeds. And then you, uh, I don't know, it gets a little tricky. Same problem as when and unless you have to kind of like invert your, uh, the way you want to make the check.
>> Yeah. So, I mean, the nice thing is there's a lot of ways to kind of solve this problem. Um, you know, cause there's if statements which when, and unless do really well with as well as guard, am I correct? Am I wrong on that?
>> Yeah. Yeah. Uh, guards work well, or, sorry. Did you mean guard the function or guards? The language feature.
>> The function, what we're speaking about today.
>> The function works. All right. I personally would recommend, and I prefer to use when instead of unless or guard, but they all end up doing basically the same thing.
>> Gotcha. And those more or less replace if statements, um, that have no drastic difference in between the cases or something along those lines.
>> Yeah. Uh, I think lisp languages often call these like one armed conditionals or something like that, because normally when you write an if, uh, expression, you have to say what to do, if it's true and what to do, if it's false, but when you write something using when, or unless you only have to specify what to do when. You know, one, one side of that condition.
>> That's one of the nifty things about it. One arm conditionals. That's good to know.
>> I hope I got that. Right. I haven't programmed in a lisp in a while. Um, but yeah, we were talking about what to name this thing because, uh, Gabe mentions that. He doesn't have a name for this trick, even though it's a pretty common one and we have the same experience. We don't have a name for this, even though we recommend it in code review frequently. And I liked the names that you gave. Um, my favorite was erroneous do notation. So that, that gets my vote for what we should call this trick.
>> Easier to say it too. So, you know, it's got that going for it.
>> You got to have things that are easy to say.
>> Yeah. We could do some alliteration maybe, but I don't know.
>> Dubious do notation.
>> Well then what you got notation in there. So you're of SOL.
>> Oh yeah. Yeah. I don't know. I'll have to think on that.
>> Yeah. Okay.
>> Listeners, if you have any ideas, let us know. We'd be happy to, uh, to shill them on our podcast here.
>> For sure! We are trying to impact the Haskell community for the better. So,
>> By giving things names.
>> Hey, in development, naming things are one of the hardest things. So, you know, nomenclature is not arbitrary. Very very challenging sometimes. But yeah, I mean, I think overall it's a great post by Gabe. Really appreciate him putting this out there. Um, you know, as a community we're trying to grow and find, you know, and share the knowledge we all have. So, um, if you're newer to Haskell and haven't really heard of this before, It's awesome. We definitely go check out the link in the bio or whatever we post. These links show notes. There we go. Um, that way you can read it for yourself and kind of see what the example looks like and may be able to apply it yourself.
>> Yeah. And I just also want to add that. I really appreciate, um, Gabe taking the time to write posts like this, for things that are kind of, um, tribal knowledge where a lot of the community, and especially the more experienced people in the community are already familiar with this pattern, but there hasn't been a name for it. There's no authoritative. Resource that I can point people to and say, Hey, like do this. Here's an explanation of why it's a good thing or just what's going on. Um, and he also posted another thing recently about, uh, proxy arguments and type applications. And I think these are good intermediate level resources. And I really liked that he's taken the time to write this stuff and I encourage everyone else who knows these types of things to write their own posts, describing these things because they're great.
>> Go intermediate Haskell!
>> Yeah. Yeah. Well, uh, that was all the notes I had for this one. Cam, do you have anything else?
>> Uh, no, no I'm excited that we got to talk about it. And, uh, looking forward to next week.
>> Yeah, next week. We'll we'll definitely record every week from here on out.
>> We say that
>> every Every time. Alright. Y'all well, thanks so much for listening to the Haskell weekly podcast. I have been your host Taylor Fausak. And with me today was Cameron Gera. If you want to find out more about us, you can check out our website, which is HaskellWeekly.News.
>> Haskell Weekly is brought to you by ITProTV, an ACI learning company, and also our employer. They would like to offer you 30% off the lifetime of your subscription. At ITPro.TV by using promo code HaskellWeekly30 at checkout. So that's 30% off all kinds of it training. So check it out. If you're interested, check, uh, we also have free membership, so that's also an option. Uh, but I think that about does it for us today. Taylor, thanks for joining us on the Haskell Weekly podcast and we'll see you next week.