18 ms·
You’re better off using Exceptions
- reffaelwallen 7y agoI love using exceptions as control-flow, Python makes it very easy, and it's really helpful. edit: grammer
- shadowgovt 7y agoMany of the problems that people who eschew exceptions are trying to avoid or fix, Python simply shrugs and declares isn't a problem. If the software you're writing agrees with that assertion (which it probably does if you're writing Python with no static type annotations and no pass through mypy or a similar tool to verify those type annotations are sound), you're fine. Unfortunately, when you do eventually find yourself in a place where you have to care (which is the fate of any codebase that becomes large or complex enough), Python actively hinders making reliable code that is easy for strangers to modify without introducing subtle bugs.
- bibyte 7y agoCan you please elaborate on those problems ? Personally I believe exceptions are the worst way to handle errors, except for all other ways.
- shadowgovt 7y agoPython's soft static type rules and extremely flexible variable instantiation unfortunately turns every line of code into a potential runtime error, because the code can't know at compile time if a variable was introduced to the global context that could bind to any given name. Typos are therefore deadly at runtime in Python in a way that they fundamentally aren't in other languages; if you try to write foo = 0 if foh == 1: # oh no, I misspelled the variable ... many languages (including F#) will fail to compile the program because 'foh is uninitialized.' Python can't know if 'foh' is intended to be a global variable and so will execute the program and only determine while evaluating that line that 'foh' doesn't exist. This is especially insidious in error handling, where the error codepath isn't necessarily exercised; essentially, Python's flexibility implies that if your unit tests don't have 100% line coverage, you can't even know if your program is basically devoid of simple variable typos naming never-existing variables (a check most languages give you for free). In a language with that feature, a rich ecosystem of exceptions is almost necessary, because every line of code could hide a runtime exception!
- bibyte 7y agoMaybe I am missing something but I think every dynamic typing system has this problem. I can't see what this has to do with Python and exceptions in general. It is not like there aren't statically typed languages with exceptions. This comment is an excellent critique of dynamic typed systems but it has nothing to do with exceptions.
- shadowgovt 7y agoThe top-thread post was referencing Python, so I'm speaking in the Python domain specifically. Other dynamic languages have this problem also, and other languages with stronger static type guarantees do support exceptions. I haven't encountered a language with dynamic typing of the sort Python has that doesn't also have a robust runtime exception system, and it'd be interesting to see what that looks like. There's probably some old flavors of BASIC that fit that mold (i.e. variable declaration is not required and also the only thing it offers for exception handling is setting a label to GOTO if a runtime exception occurs).
- bibyte 7y agoI guess I was too fixated on the exceptions part of your comment. You are right that due to Python's dynamic nature every line can turn into a runtime exception. Maybe it's because English is my second language but I couldn't understand that in your first comment.
- the_af 7y agoThis has been my experience as well. I often fall into the "Python trap" (you'd think I would have learned by now!): "this is a tiny project, almost a script, and it's so much nicer and faster to code it in Python. And since it's so small, who cares about static typing? Surely this won't grow larger". A couple of months later: "oh, no!" [1] [1] If you know the webcomic "webcomic name" by Alex Norris, it fits perfectly here.
- chrisseaton 7y agoI also use exceptions for control-flow and I feel no shame about it. It's often the best (most concise, most performant, most understandable) solution to a problem, in my eyes.
- ComputerGuru 7y agoMost concise: absolutely. Most performant: never. Most understandable: yes, but only if you are cognizant of which call paths can result in an exception and which can’t (i.e. until your code base becomes too large or you’re not the one that wrote it).
- chrisseaton 7y ago> Most performant: never. I disagree. An exception can be faster than manually unwinding a set of nested scopes.
- lllr_finger 7y agoMost exceptions, at least in the JVM where this is commonly debated, will incur a penalty of building up the exception and unwinding the call stack. What aspect of performance are you using as an example?
- chrisseaton 7y agoAn exception thrown and caught within a compilation unit is effectively a goto. It can translate into nothing more than a jcc instruction, or even nothing at all if it's unconditional. This can be higher performance in practice than threading through a set of Either-style return objects.
- hopia 7y agoThrowing exceptions may be the most concise, but handling them rarely is. But most importantly, static type systems lose the ability to infer what's coming so these may turn out to be really pesky problems.
- TeeWEE 7y agoExceptions for control flow is bad, IF that control flow is an expected business rule. Exceptions shoudl be exceptions. And python programmers often write a whole exception class model to define all business logic rules... Really use Enum for that!
- sethammons 7y agoHaving worked in Twisted's framework, I utterly despise exceptions as control flow. One service I work in is 140k lines of Twisted python (aptly named). I can't wait for us to finally kill it and never deal with Twisted again.
- deleted 7y ago[deleted]
- kstrauser 7y agoFor example, in a web service I maintain, we maintain an internal exception hierarchy like: class OurExceptions: http_status_code = 500 class DatabaseRecordNotFoundError: http_status_code = 404 Database queries are all written like: rows = db.select(...) if not rows: raise DatabaseRecordNotFoundError and the top-level Flask error handler has code like: try: return call_view() except OurExceptions as exc: return response(status_code=exc.http_status_code) This is grossly oversimplified, but you get the idea. So, this means that we can write views like: def some_view(object_id): obj = fetch_obj_from_db(object_id) return {"found": obj.name} If the object isn't found, the caller gets a 404 response without the person writing the view having to do a single thing. However, they can still handle the problem themselves if they really want to: def another_view(object_id): try: obj = fetch_obj_from_db(object_id) except DatabaseRecordNotFoundError: return {"error": "Not found. Try again later?"}, 404 return {"found": obj.name} I absolutely love this coding style because exceptions are still being handled everywhere, but don't have to be explicitly dealt with deep inside a nested call stack. That lets us write very uncluttered, testable view code like: def change_password(userid, oldpass, newpass): # This raises an exception if the user can't be found user = get_user_by_id(userid) # This raises an exception if the old password is wrong verify_password(user, oldpass) # This raises an exception if the DB couldn't be updated, # perhaps because of a race condition with another request update_password(user, newpass) # By the time we get to this line, everything above has to have # succeeded, with zero manual error checking inside this view return {"result": "Password successfully updated."}
- pat2man 7y agoWhen I moved to Rust the constant error wrapping or converting annoyed me. But after using it for a couple of years it turned out to be a huge blessing. Quite often I need to know exactly which error messages will be thrown so that I can do things like internationalization. While exceptions are quite convenient for prototyping for production I’m now firmly in the typed errors camp.
- shadowgovt 7y agoAgreed. I nowadays consider code that might throw any type of exception to be inherently unsafe code; if you don't know what can go wrong, how do you have any idea whether you've addressed all reasonable failure modes?
- shepmaster 7y agoIt's not clear if you are continuing to refer to Rust as your parent post did, but if so, note that this is not the standard Rust meaning for "unsafe". Using it in such a way makes it more complicated to discuss and explain the Rust-specific meaning.
- shadowgovt 7y agoThank you for the clarification; I was using 'unsafe' colloquially and not in the Rust sense.
- ferzul 7y agoIf Rust devs took an everyday English word and gave it a different meaning, isn't it their fault when they have to train everyone into discarding their intuitions? It sounds like it means “code that is unsafe for a particular reason”; name the reason and leave developers capable of criticising code that is unsafe for other reasons, too. Unchecked Exception throwing code and stringly typed code are both unsafe. The world is better if we can call spades spades, instead of pitchfork style digging implements, because you want to protect playing card manufacturers.
- shadowgovt 7y agoeirik's making some very good points regarding the downsides to avoiding exceptions (particularly in the notion that one loses stack trace details and if one doesn't think about how debugging will be done without them, one is in for a world of hurt). I think I only have some significant disagreement with one observation they've made, which is around runtime errors: "It’s by such misadventure that the working F# programmer soon realizes that any function could still potentially throw. This is an awkward realization, which has to be addressed by catching as soon as possible." I prefer to think of runtime errors as special case, and I appreciate Go's approach here: Go lacks any exception-handling more complex than panic and recover, and considers anything that is walking the panic / recover path to be an "exception handling failed" scenario. In other words, a correct program should have no runtime exceptions, and if one happens, it should explode as messily, noisily, and identifiably as allowed (and rare is the situation where the correct answer isn't "whole process dumps stacktrace and dies"). My suggestion for the case the author points to is to not catch that exception; if you take a runtime exception you don't anticipate, let it fly. It indicates you're "holding the API wrong" and you want to know about it ASAP, not try to catch around an unexpected failure mode. For expected failure modes, results and error types are often more comprehensible (though the issue of needing to address stacktraces still exists).
- baby 7y ago> In other words, a correct program should have no runtime exceptions, and if one happens, it should explode as messily, noisily, and identifiably as allowed This is not always true. For example if you are writing high assurance software, and the error is recoverable, you really don’t want to crash. You want to catch it, log it, and continue/recover execution. Of course, now you need to make sure that you’re not catching an exception that is not recoverable.
- unlinked_dll 7y ago> Such criticisms typically revolve around scoping considerations, exceptions-as-control-flow abuse or even the assertion that exceptions are really just a type safe version of goto. Outside of the FP community where people care less about purity - the criticism is more that exceptions are hidden, they're all-or-nothing (either any function can throw, or no functions can, and not all code paths have runtime errors that you care about), and of course the overhead. I also really disagree with the idea that result types have more boilerplate than exceptions. That just depends on the paradigms of the language you use, anyone can add sugar to make either easier/harder to read.
- paulgb 7y ago> That just depends on the paradigms of the language you use, anyone can add sugar to make either easier/harder to read. The questionmark operator, in particular, is what sold me on the possibility that this type of programming can be concise and ergonomic. https://doc.rust-lang.org/edition-guide/rust-2018/error-handling-and-panics/the-question-mark-operator-for-easier-error-handling.html https://doc.rust-lang.org/edition-guide/rust-2018/error-hand...
- chaorace 7y agoI've not used Rust before, but this looks a lot like monads! Can I map/flatmap over these or do I have to unpack them later? If you _can_ operate on those like collections, Rust is a much more fun language than I initially gave it credit for!
- paulgb 7y agoYes, you can map over them, and it's great! I think they satisfy all of the monad rules, but I'm forgetting what they all are at the moment.
- meijer 7y agoMy rule of thumb is: if it is the type of error that I want a stacktrace for: use an exception. For example: programming errors, unhandled cases etc. If it is an error that I want to handle explicitly, for example by showing a nice user message: use a result type. It is okay to use combinators in this case, but use them wisely.
- waynecochran 7y agoException != Error Old Ada programmer here. Example of reading bytes from a file... just keep reading bytes and don’t include logic for checking for EOF. Let the exception handler catch it where the file will be closed. Clean separation of code. In Ada, every bock can have exception handlers at the bottom. No need for “try” syntax. Very clean.
- quietbritishjim 7y agoThat is like C++ destructors, which are called deterministically when they go out of scope (again with no "try" syntax). They can unlock mutexes, close files, etc. But unlike what you describe, there is no need to put anything at the end of the function: instead, the fact that you have instantiated an object already guarantees that its destructor will be called later. This is called RAII, short for "resource acquisition is initialisation".
- waynecochran 7y agoYes... RAII is handy, but it doesn’t save you from checking EOF logic in my example.
- safety-second 7y agoYou could use a File class that throws on EOF, but that would be a really bad idea for C++ since throw/catch are very expensive.
- quietbritishjim 7y agoExceptions are expensive compared to a simple function call, but I imagine they're cheap compared to operations that read from disk. Certainly one exception per file close is not going to hurt.
- gwbas1c 7y ago> don’t include logic for checking for EOF. Let the exception handler catch it where the file will be closed What happens if the file is smaller than you anticipate? That's why exceptions aren't used for flow control.
- ravenstine 7y agoMy problem with exceptions isn't so much exceptions themselves but the way they're used. The way I see it, exceptions should only be used for things that are irreconcilable, which most of the time is interpreter errors(e.g. undefined is not a function). In other words, I don't think it's that common that custom exceptions are needed outside of assertions to prevent the developer from doing something stupid. If you aren't using types, raising an exception can be helpful to provide feedback and useful information when data isn't formatted correctly. Exceptions suck when they're used for problems that aren't actual problems. For example, if a query is made to an API for a record and that record isn't found, the SDK wrapping that API would raise a "Record not found" exception. What the hell? A record not being found is a normal thing! I've seen this kind of thing all over the place, and I recently had to write some application code to work around one of these useless exceptions. They literally tell you nothing and there's no way to solve them without catching/rescuing them. A null value, a plain "error" object, or an error argument in a callback would have been sufficient. If I need an exception to be raised for this kind of thing, I'll do it myself.
- shadowgovt 7y agoI find the mindset that exceptions are the right way to handle ordinary scenarios symptomatic of what I call "happy path coding:" assuming that the stars are always perfectly aligned for your program to work, and nothing ever goes wrong, and if it does it's someone else's problem. It's a fine approach for prototyping but we should be highly suspicious of it for code people rely upon to work. As complexity approaches infinity, the odds of the system being "happy path"-aligned approach zero. Network-relevant programming (such as web UIs), in particular, really highlights this fact (since any given network request is always allowed to fail, and "user closes their laptop or mobile device, moves somewhere else, and resumes work on an entirely different physical network" has gone from a rare occurrence to an extremely common one in the past two decades).
- AstralStorm 7y agoOn the other hand, do you really want to bubble error results through layers of network stack and parsers, when handling is almost always the same?
- correct_horse 7y agoThe title contrasts the last paragraph. I believe the author means this to be true only in the CLR, F# specifically. > I strongly believe that using result types as a general-purpose error handling mechanism for F# applications should be considered harmful. Exceptions should remain the dominant mechanism for error propagation when programming in the large. The F# language has been designed with exceptions in mind, and has achieved that goal very effectively.
- rahilb 7y agoReminds me of this classic enum Bool { True, False, FileNotFound };
- gambler 7y agoWhat you really need in such cases is a pentastate boolean: https://crossdysfunctional.tumblr.com/search/pentastate https://crossdysfunctional.tumblr.com/search/pentastate
- foolfoolz 7y agoI’ve pushed for Either[L,R] based coding without exceptions for a while and met a lot of criticism along the way. I still think it has value over exceptions but this post does bring up good points. The one about throwing the stacktrace away is annoying, but with Future your stacktrace is useless anyway. We have a system where every error has a unique id that looks like a jira ticket number so you can find the source very quickly and avoid the stringly typed error. This section of code is what i want to avoid: user = service.getUser() bill = billingService.getBill(user) notificationService.message(user) we use the same http client in all underlying service clients. let’s say you get to the end of this block and it results in SocketTimeoutException. How do you know what call did this? you would have to add to every line: .recoverWith({case t => new Exception(“billing exception”, t}) to propagate the context up. Either forces you to handle that well and write the context. a lot of it comes down to style choice because you could do it with exceptions, but i think this makes it easier not forget cases because the compiler will tell you
- ferzul 7y agoExceptions in Haskell are unavoidable and awful. If you want to write code that responds to the errors that could occur, even some of them, you basically have to read the library source because they're completely undocumented in many libraries - even in the standard library (try something exotic like, um, reading a file). And that means you need to know your whole stack. If they were at least declared in the type so the compiler could give me a warning.
- hopia 7y agoThat does suck, does it commonly occur because of non-total functions in the libraries or what?
- baby 7y agoI’d say this works in Erlang because it is part of the culture, but in java or python what I’ve seen mostly is people forgetting to catch exceptions, or not being able to pinpoint where exceptions are catched.
- hopia 7y agoErlang has a very different execution model compared to those other languages you mentioned. In fact, Erlang's culture in many ways encouraged to program less defensively than you would be expected to in other languages (ever heard of "let it crash"?). That's because the whole expectation of exceptions/errors is built into the system unlike pretty much anywhere else.
- LOL_Arch_Linux 7y agoWhile my favorite language (Erlang) has Exceptions, I really like the "let it crash" model -- let the whole light-weight thread die and have the controlling thread (a/k/a "supervisor") figure out what to do. I've seen insane Java programs where tracking how many layers up Exceptions are caught was mind-twisting. And conversely, when everything was wrapped in a try/catch and then, essentially ignored.
- smegma2 7y agoCan't the rethrowing logic be expressed using `fmap`? λ :set -XTypeApplications λ :t fmap @ (Either Error) fmap @ (Either Error) :: (a -> b) -> Either Error a -> Either Error b
- BiteCode_dev 7y agoI don't think it should be "exception vs returned value". First, if you are using a dynamic language, then exceptions make sense. You want to get there quickly, not handle every corner cases. At the opposite spectrum, if you are writing rust, of course you want something very explicit and extremely aggressive to deal with errors. They just serve different needs. Also, exceptions don't have to be implicit, difficulties arise when the code you are using do not or cannot declare what can of exception can happen when you call it. But if the language let you declare all the stuff you can raise, then there is not much difference.
- holtalanm 7y agoi think there is a distinct difference between an exception, and a recoverable error. Result is not intended to be used for exceptions, but rather recoverable errors. There are a number of situations where I have used Result to recover from an exception that was expected, but it would be folly to try to handle all error paths (especially with runtime errors). I think the author is trying to convey the point that we shouldn't use Result as a catch-all, but rather as a means of conveying possible known error paths for a function. There will always be the possible unknown error paths, which are handled by the runtime's exception handling/reporting.
- dana321 7y agoWhat's exception handling actually doing internally? Genuinely interested and i need to know. Must be some kind of "goto catch block" internally when you throw an error. It has to stop the execution and jump somewhere, but that somewhere is set in the code where it catches the error. If you're calling a function then that function throws an error, it has to prematurely exit to somewhere so there must be a stack of the locations of the catch blocks or something? That then unroll as you exit the try/catch blocks as well.
- gnulinux 7y agoFor gcc/clang C++, internally, it will unwind the stack until it finds a handler. This way it doesn't have to do anything at the moment of setting a handler (i.e. "try" in the underlying code), as a trade-off it's relatively expensive to throw an exception. But note that C++ standard does not specify this, so it's fair game for compiler to do anything. E.g. it can use long jumps, or just dispatch different functions based on all possible exception states (which produces very large binaries).
- dana321 7y agoThanks for that, nice to know!
- yunruse 7y agoI personally feel that use of exceptions kinda depends on how compiled the language is. With strongly-typed, compile-checked code, it’s only really bare assertions for a test suite to build. Meanwhile in weakly-typed languages without means for overloading, certain type-based errors have to be made to ensure the signatures are met. Errors for trivial things (ie control flow) are an unfortunate abuse of try and catch; generally exceptions should only be thrown to users of an external API, not an internal one. If an exception is caught from the same layer of abstraction it was thrown from, it’s a very leaky abstraction — as opposed to, say, a regex parsing error, which ensures the abstraction isn’t implicitly failing.
- michaelfeathers 7y agoGood article but it is tangential to a basic design issue. Error handling should be done at the edges of systems rather than in the center. If you do that, it doesn't matter whether you use exceptions or error monads, you have a pure core that doesn't need to deal with error handling and a very slim area to catch errors. The mechanism you use for error handling, at that point, doesn't matter. The problems of error handling often come from having it spread across the system and mixed with the logical flow.
- Barrin92 7y agoit's not trivial in every domain to distinguish between pure and non-pure operations. If you're writing software for control systems, robotics, anything that has a very tight, low-level coupling between programming logic and the outside world it's harder or often not possible to separate concerns into some sort of data -> logic pipeline that lends itself to this style of programming, or it may come at the cost of performance.
- michaelfeathers 7y agoIt's a decent goal, though. For all else, there's Erlang.
- haagen 7y agoIn Java, you can pre-allocate static final Exception objects with a pre-filled stack. These exceptions have much better performance characteristics [1] and are used by Netty [2] for control flow purposes. Sometimes these are helpful and the performance is good, but only when you don't need a full stacktrace or to customize the error message. [1] http://normanmaurer.me/blog/2013/11/09/The-hidden-performance-costs-of-instantiating-Throwables/ http://normanmaurer.me/blog/2013/11/09/The-hidden-performanc... [2] https://github.com/netty/netty https://github.com/netty/netty
- rectang 7y ago> It’s by such misadventure that the working F# programmer soon realizes that any function could still potentially throw. That's your problem right there. "Invisible control flow", as Joe Duffy puts it. http://joeduffyblog.com/2016/02/07/the-error-model/#unchecked-exceptions http://joeduffyblog.com/2016/02/07/the-error-model/#unchecke... What's needed are two distinct error mechanisms: panic/abandonment for unrecoverable errors (which can happen at any time), and then for recoverable errors, either checked exceptions in some flavor (including Swift and Midori's untyped `throws`) or Result types. Once you eliminate invisible control flow, you're no longer "better off using Exceptions".
- goto11 7y agoBut then the failed code which decides if the error is recoverable, not the part of the code doing the recovering.
- rectang 7y agoYes! We shouldn't expect downstream to wrap every array access which could be out of bounds in a "try" block. We shouldn't expect downstream to wrap every division operation in order to catch potential divide by zero errors. Those should be fatal errors, catchable only at a very coarse-grained level, because once they occur the program state is potentially compromised in a way which the programmer did not plan for. The library author gets to decide whether a potential failure mode should expose a recovery API.
- dehrmann 7y ago> ...exceptions-as-control-flow abuse or even the assertion that exceptions are really just a type safe version of goto. I was recently telling someone about a little-known Java feature that I wish C would adopt. Goto is common in C exception handling code because the alternative is unworkably messy. Java has named code blocks that you can break out of, so they're not gogo, but they're also not exception abuse. initBlock: { if (fail) { break initBlock; } doSomething(); }
- ferzul 7y agoIn that case, in javascript, i always just write a function. { init() } const init = () => { if (fail) return doSomething() } (there's a few other cases i introduce functions for syntactic reasons in js - e.g. at the expense of a const funtion, i can convert a let to a const - but i have always found this improves the code on standard readability guides)
- js8 7y agoThere are actually three error handling paradigms: 1. Return values, like Either in functional languages 2. Exceptions 3. Error handlers, for example Lisp condition system. (Unfortunately, this option became less popular in programming languages, probably because Unix botched it.) Now here lies the problem. Paradigm 2 is better than 1, because you don't have to handle the error at the caller (or at least, you don't have to unwrap the type). Paradigm 3 is better than 2, because the stack doesn't get unwounded when the error handler gets called - the error handler can choose to continue the original code. Finally, paradigm 1 is better than 3, because it is just so much simpler to set up and reason about. So it turns out, you're better off using all of them! Although personally I believe exceptions are conceptually wrong, and in almost all cases, either 1 or 3 should be used. (Sadly, option 3 is not well supported in most languages.)
- norswap 7y agoI wrote something in that vein, in which I defend checked exceptions (in contrast with unchecked exceptions): https://norswap.com/checked-exceptions/ https://norswap.com/checked-exceptions/ > Summarized: people don't want checked exceptions because they are going to be abused by lazy programmers.
- petilon 7y agoThere is also a long discussion on pros and cons of checked exceptions here: https://forum.dlang.org/thread/hxhjcchsulqejwxywfbn@forum.dlang.org https://forum.dlang.org/thread/hxhjcchsulqejwxywfbn@forum.dl...
- bogdanoff_2 7y agoFor the readAllText example, could it return Result<string, Exception>?
- xeonoex 7y agoYes, but you can still argue just throwing it might be better. This also affects the stack trace, because, at least in C# (so probably F#) `throw ex;` is different than `throw;`, which rethrows the exception as if it weren't caught.
- alipang 7y agoIt's almost a meme at this point, but the solution, of course, is algebraic effects. Among other things they let us implement checked exceptions, but in a sane way that's safe without requiring the annotation of every single function. https://www.microsoft.com/en-us/research/wp-content/uploads/2016/08/algeff-tr-2016-v2.pdf https://www.microsoft.com/en-us/research/wp-content/uploads/...
- ragnese 7y agoIn the article he posts a snippet of F# code: type Customer = { Id : string; Credit : decimal option } let average (customers : Customer list) = match customers with | [] -> Error "list was empty" | _ -> customers |> List.averageBy (fun c -> c.Credit.Value) |> Ok And argues that the F# programmer must conclude that all functions may throw because this function accesses the Option.Value without checking that it isn't None first and there's nothing in the type signature of the function to indicate that it may throw. Is that really how F# works? Similar code wouldn't compile in Rust. You have to handle both cases for Option<T> every time you want the value (or, yes, you can get the value by force, but if you do that, it's definitely NOT an accident and you're basically acknowledging that you want the whole program to crash if the value is None). Anyway, I'm still in the Either/Result/Try camp. I even used Result/Try types in Java, Kotlin, and Swift (before they officially showed up in Swift and Kotlin). I think the extra boiler plate in the middle of a function chain is absolutely worth it. I hate the idea that I need to actually read the source code of every function I call from library X just to see if it will throw an exception on me. Swift has a pretty good compromise, IMO. A function's signature must indicate that it throws, but it doesn't have any type indicated. At least I know when I need to investigate a function further!
- UK-Al05 7y agoThats not how f# works normally. Normally you chain option operations with either binds, or you do a match(Which is exhaustive). Directly accessing the value is normally frowned upon. This guy either needed to make the option type go away, or filter out the None and pass that into average. List.choose could be used for this let average (customers : Customer list) = match customers with | [] -> Error "list was empty" | _ -> customers |> List.choose(fun c -> c.Credit) # Would remove the option type. Any nones would be filtered out the list |> List.average |> Ok This is the best I could think off without a computer
- vikestep 7y agoIn support of the article, your code would still throw an exception if all the customers in the list have missing credit. https://github.com/dotnet/fsharp/blob/master/src/fsharp/FSharp.Core/list.fs#L627 https://github.com/dotnet/fsharp/blob/master/src/fsharp/FSha... You could instead write it as the following to handle that. let average (customers : Customer list) = customers |> List.choose (fun c -> c.Credit) |> function | [] -> Error "no customers with credit" | xs -> Ok (List.average xs)
- ronlobo 7y agoInteresting article, also very F# specific. A similar approach with type aliases (to be moved to value objects or structs with behaviour aka rich models) in Rust. ```rust type Amount = u128; type MoneySign = Option<Sign>; type Precision = u8; type CurrencyIsoCode = &'static str; type CurrencyName = &'static str; type Currency = (CurrencyIsoCode, CurrencyName); type Money = (Amount, MoneySign, Precision, Currency); type Balance = (Money); type ExpirationDate = &'static str; type MoneyWithdrawalError = &'static str; enum MoneyWithdrawalResult { Success(Money), InsufficientFunds(Balance(Money)), CardExpired(ExpirationDate), UndisclosedFailure(MoneyWithdrawalError), } ```
- cletus 7y agoI wonder how many commenters clicked on this link because this really has nothing to do with "exceptions" (as in the things you raise or throw) at all. So it's a fairly terrible title that is really arguing against a catch-all Error case. The one parallel to (actual) exceptions is the author is arguing for a constrained subset of error states rather than a catch all Error type. This isn't a new idea. Java essentially does this with checked exceptions. C++ does this where you can declare what exceptions can be thrown (which you should basically never do). So I did Java for years. Java had several Grand Experiments, one of which was checked exceptions. Despite it still having some fans I think the general consensus now is that checked exceptions were a Huge Mistake [tm] for many reasons (eg leaking implementation details, cluttering your API the whole way up). One anti-pattern I see with exceptions is people using them for control flow. For example, dealing with a ParseException in Java when parsing numbers [1]. This of course falls into a religious argument about what an exception actually is. For a few years at Google I wrote Google's flavor of C++, which I actually grew to really like. It is pervasive that any method in Google C++ returns a util::Status. This is much like Go error handling but (IMHO) better. For one thing, it's a compiler error to ignore the result of a util::status (you can call .IgnoreResult() if you really want to ignore it). I think this is a much nicer and safer default. You return things with a util::StatusOr templated union type that has the same semantics. There are even macros (that were somewhat controversial) to reduce boilerplate to do things like call a function that returns a StatusOr, assign the result to a variable if it's OK or return the error if it's not. There are of course utility methods for adding context to a Status(Or) you're returning. So all this came about because Google C++ strictly prohibits exceptions. This is a historic decision that's probably impossible to unwind at this point and honestly I don't think there's a strong motivation to change it. IMHO exceptions are a false economy. This is one of many things I like about Rust. Rust's enums are kind of the next evolutionary step for this. It's a compiler error not to deal with all options, there are constructs to reduce boilerplate and you can pass values. [1]: https://stackoverflow.com/questions/8286678/parse-string-into-number https://stackoverflow.com/questions/8286678/parse-string-int...
- mytherin 7y agoI think Java's flavor of exceptions are especially horrible, as you mentioned a string->int parse failing is typically not exceptional at all but rather expected. However, I do think there is a place for exceptions: exceptional situations. Status(Or) is nice for certain types of errors, but now if you want to correctly deal with out-of-memory errors that means every single function that allocates anything on the heap now needs to return a Status(Or) in case the allocation fails. That error message then needs to be propagated through your entire library back to wherever it is that you can handle memory errors, which is likely somewhere at the root of the library. That now means that every single function in your entire library needs to return a Status(Or) and propagate it, which significantly bloats your codebase. The macros you describe likely help with that somewhat, but then those macros are basically manual exceptions in that they just unroll the stack to where you don't call the macros anymore, but they require a bunch of extra manual effort and obfuscate your code somewhat. They are also slower in the common case (no memory errors), as you are now performing an extra check on every single function call in your entire library. Meanwhile using exceptions together with smart pointers and smart locks basically makes it so that you can handle memory errors "for free" without having to bloat your codebase. When a memory error pops up, throw an exception and unroll back to where you can properly handle the memory error. The smart pointers/smart locks/destructors will take care of cleaning up everything. No need for any extra checks all over the code base for such a rare error.
- rationalfaith 7y agoTools tools tools They're just tools Don't turn a tool into a religion. It's very very very stupid and simple minded.
- Gepsens 7y agoSo much confusion between Exceptions and Errors in this thread. Exception abuse is for bad code that needs to return early, because they have no pattern matching, like Java and JavaScript. Nice languages have error handling, think Go, Elixir, Rust, Scala.
- dustinmoris 7y agoI completely disagree with this article, as it makes a lot of claims which are not true, uneducated or intentionally misrepresented to make a case for exceptions: > Because runtime errors are difficult to anticipate, I claim that using result types as a holistic replacement for error handling in an application is a leaky abstraction. Yes this is true, but it's not a leaky abstraction. There's always the posibillity that shit hit the fan and something happened which was unexpected and indicates an unhealthy system in which case it will result in an unanticipated exception such as a RuntimeException. This is ok, that's why every application has somewhere a global error handler, which can capture that unusual exception, log it and potentially terminate the application or put it in an unhealty state so that a higher level scheduler can replace or re-start the app. Possibly even raise some emergency alerts with developers via PagerDuty, etc. However, it most certainly doesn't leak anything or makes the Result type less useful, because most application errors are anticipated due to a combination of user inputs or other external factors such as API calls and these can be perfectly handled in a more predictable way by using a Result type. > It’s by such misadventure that the working F# programmer soon realizes that any function could still potentially throw. This is an awkward realization, which has to be addressed by catching as soon as possible. Not really. There's nothing awkward about a runtime exception. Shit sometimes happen. And it's not true that it has to be handled as early as possible. As said before, you only have to deal with exceptions in a global error handler. If the error must be caught as early as possible, then it means that there is a possible plan B and a possible plan B can only exist if the error is anticipated. If it is anticipated then it should be returned in a Result type. So fundamentally exceptions are not awkward. When they happen there's exactly only one place where they need to get handled and the rest of the application code just works with Result types where errors are possibly known. > In the majority of codebases using result types that I’ve been reviewing, people typically just end up re-implementing exception semantics on an ad-hoc basis. This can result in extremely noisy code... So he has just worked with badly written code. If all the code is doing is bubbling up an error from the Result type then whats the point of returning that error? Return errors which are meaningful and where the calling code can deal with it immediately, otherwise don't bother. > An important property of exceptions -which cannot be stressed enough- is that they are entities managed and understood by the underlying runtime, endowed with metadata critical to diagnosing bugs in complex systems. They can also be tracked and highlighted by tooling such as debuggers and profilers, providing invaluable insight when probing a large system. By lifting all of our error handling to passing result values, we are essentially discarding all that functionality. Well as said before, the Result type is not to be logged or thrown or something. It's there so calling code can deal with it - implement some sort of plan B. If all the author wants is to always log the entire exception including stack trace for every error and not really deal with it then fair enough, just use exceptions everywhere, but that application will suck big time. > That said, I strongly believe that using result types as a general-purpose error handling mechanism for F# applications should be considered harmful. Functions are Input -> Output. If you don't like that as a "general" mechanism, and you prefer Input -> Output, Exception, {whatever} then use OOP and not FP. It almost seems like that the author just doesn't like the functional appraoch in functional programming, which is a weird point to make. > Exceptions should remain the dominant mechanism for error propagation when programming in the large. Based on which logic? That's such a generalisation that it's just plain wrong. Exceptions are try-catch error handling and there is no proof that try-catch is the ultimate error handling solution. Lots of new languages make an effort to exactly not do that, so where's the evidence?
- jackcosgrove 7y agoI think it's fine to use exceptions as error handling if the stack can safely be destroyed with that exception. As the article states, they provide a lot of information to help debugging and are resilient to refactoring. An example of safe stack destruction is a web service call that cannot complete because of the error. You're going to return an error response regardless. I don't think exceptions should be used if they are caught and then retried or transformed. I don't think exceptions should be used in library code, except of course for unanticipated behavior such as hardware or network problems. In these cases it's better to use a result object, which better forces the library consumer to inspect what is being returned, rather than pass on what is returned and (maybe?) handle exceptions on errors. Ideally the library catches unanticipated errors and never throws, but those are edge cases that are probably not worth the investment by the library maintainer to handle. Development effort across the ecosystem is probably minimized when library consumers handle exceptional situations themselves, which they may have more insight into if their own infrastructure is causing the exception.
- trumpeta 7y agoHere's my problem with exceptions: (I'm going to treat checked exceptions same as unchecked, because you don't _have to_ use checked or they can be trivially broken by using the Exception superclass) At the beginning you often take shortcuts and ignore error handling in order to get the happy path to work. Once it is working, it is very easy to overlook some exceptions that should be handled, leading to unexpected runtime errors. Using the Result monad (say in Rust), you can easily just add .unwrap() after any call that produces a Result to get the happy path working. After you're done with that, its easy to find all the places you took shortcuts and fix them. To me, the ergonomics of a language is exactly this gap between how concise the "take all the shortcuts" code is and how easy it is to transform into production code where all the possible error states are properly accounted for.
- MaulingMonkey 7y agoI theoretically like exceptions, and used to extol their virtues, but I've come to hate them in practice. They're an endless source of fustration in that the APIs you least expect will throw various unexpected exceptions for various undocumented edge cases, which invariably leads to heisenbugs crashes in our bug tracker that I need to go waste a few hours trying to repro and root cause because I, foolishly, was a good boy who didn't spam catch(Exception) everywhere (as that would also catch the null/index exceptions indicating real bugs). Theoretically, forcing you to use checked exceptions would solve this, but would defeat many of the supposed virtues of exceptions. I also frequently get stuck refactoring a lot of code other people have written because an exception tried to unwind across an ABI boundary, and nobody thought about how to handle that or what would happen. That ABI could be across a C module, which has no exceptions. Or perhaps across a C++ module built without exceptions, or using an incompatible unwinding mechanism. Cleanup gets skipped, undefined behavior gets invoked... it's a mess. So, invariably, I write some catch(Exception) equivalents, if only to manually write the code to explode loudly and in a way condusive to debugging, instead of much later when the stack trace has been mulched through rethrows, or access violations from the UB, or ... Now compare and contrast that to error codes, which I theoretically dislike, but swear by in practice. Error codes are C ABI safe, any language can return them, set them, or store them. They're part of the method signature, neglected documentation be damned. There won't be any exotic failure modes where C++ destructors get skipped due to a setjmp style unwind. Even the absolute worst APIs usually have some kind of incomplete list about failure modes - a list of constants somewhere - and I have a fighting chance of deciding upon a sane local fallback (on top of breakpoint/logging/reporting) in the event of an undocumented/unexpected error code. My biggest concern was always accidentally ignoring an error code, but every language at least has compiler extensions these days - to mark a function result as needing to be used - and a way of turning that warning into an error. Does this sometimes lead to a little extra error handling boilerplate? Yes. Does that even enter into the top 10 issues I have with error handling code? No. I do get the occasional bug where ignoring an error code contributed to it's occurance, but those are a small price to pay compared to avoiding all the bugs arising from exception (mis)use I avoid.
- gwbas1c 7y agoThe real problems with exceptions are that they have a large performance overhead, and that they differentiate poorly between anticipated errors and truly exceptional situations. The performance overhead comes from grabbing a stack trace and constructing an object that lives on the heap, and then unwinding the stack. (Some assembly experts can probably explain this overhead better than I can.). When your code handles an error condition that it anticipates, that overhead is wasted CPU cycles. (This is why Exceptions aren't for flow control.) What we really need are languages that differentiate better among success / error / exception. Go has panic / resume, and Java has compiler-enforced exception handling, unless it's a runtime exception. What I want instead is something where I have optional and convenient syntax to handle lightweight errors; or the ability to automatically convert errors to exceptions if I don't handle them. TLDR: Think of the query to lookup a record by ID case. If the record isn't found, it's an error that doesn't have a stacktrace or an object on the heap. But, if my code doesn't handle the error, then it's a full exception with a stacktrace.
- patrec 7y agoExceptions don't generally have a large performance overhead, they do in C++. Ocaml exception handling for example was orders of magnitude faster last I looked. And C++ itself is seeking to fix the super-slow design: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0709r0.pdf http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p070....
- luord 7y agoAs someone who's been writing Go for too long, way too long, I agree. Handling errors everywhere seems to always become ad-hoc exception handling. Oddly, the apologists call this "syntactic sugar for avoiding repetitive error handling", still can't wrap my head around that one.