23 ms·
Away from Exceptions: Errors as Values
- ewg4345h43 5y agoIt looks like GO developers never heard about "not repeat yourself", because after almost each call in GO, you write boilerplate error checking code... So you end up writing at least 2 times more lines of code.
- feffe 5y agoThe article is not about Go, although that was my guess before reading it as well.
- iwwr 5y agoYou can always... not treat errors or exceptions. You also create employment for oncall engineers and debugging tools, I'd call it a win-win-win :) /s
- habibur 5y agoYou need all those error check code if you want to recover from error -- regardless of whether the error is coming from exception or value. If you don't want to recover, just throw it down the stack that will exit, then can you do that without exception too. Just call error() function on error which will print the error and exit(-1). No need for per-line error checking in this case too.
- dtech 5y agoYou don't need it everywhere. With both exceptions and error monads, you can have a happy-flow path that mostly leaves out the boilerplate, and handle errors at a reasonable point. Go forces you to add even if you want to defer handling to a later point.
- deleted 5y ago[deleted]
- tsimionescu 5y agoRealistic options are not limited to "handle at every point of the call stack where an error is encountered" like Go and "end program execution as soon as an error is encountered". Most programs handle most errors by bubbling them up to some top-level event loop and presenting some variant of abort/retry/fail to users. Exceptions are tailor-made to cover this use-case.
- habibur 5y ago> errors by bubbling them up to some top-level event loop and presenting some variant of abort/retry/fail to users The pros and cons of this approach have been covered extensively in the last decade. the points discussed were -- // <well what's the point of arguing on the net anyway??>
- agumonkey 5y agoIt was a very conscious decision by go devs. To enforce local error fixes instead of the usual wrap and throw the exception again.
- deleted 5y ago[deleted]
- praptak 5y agoTo me exceptions are convenient but in the same way global variables are. Both mechanisms relieve you from (explicitly) passing stuff between callers. The stuff still gets passed only in a way that is less visible and harder to reason about.
- koblas 5y agoInteresting that the author used joi as the example, when the io-ts validation library fully embraces the functional world with success/fail return value from validation. Realistically, the advantage throw has is that if you have a single error handler in a function it avoids the "go" problem of tons of lines of error handling. The other side is that error handling becomes "optional".
- watermelon0 5y agoPersonally, I gave up on io-ts due to FP complications, and use suretype instead. In my case, where I was parsing HTTP request body, it just simplifies the code, if I can call a single function, get back validated object of the expected type, or throw an exception if there is a problem. Global request handler takes care of catching validation exception, and returning back user friendly error on what field(s) failed validation.
- koblas 5y agosuretype looks interesting, will have to do a bit of review there. One of the things we're now doing is using io-ts for both encoding and decoding types. Internally we're more JSON than gRPC so we're using it to provide a standardized way to move data between components.
- slx26 5y agoIt's good to see so much focus on errors. They are essential when trying to build resilient systems. But our approaches are still very immature. First, to make it clear, this article appropriately points out that exceptions are still necessary and relevant. I disagree with some of the use-cases given, but it's important to recognize that exceptions should still exist in programming languages. Joe Duffy's article about Midori's error model [0] is in my opinion the best reference to actually understand the difference between exceptions and errors and why it's so important to get right. It's a very good article, and it has been posted here before; if you are interested in error handling it's a must read. Now, about errors as values. Treating errors as values is practical, and in modern programming languages, relatively ergonomic. That said, we already have some other comments in the thread pointing out how sticking to just "errors as values" is often not enough (btw, the article uses Rust, not Go, but anyway...). And it's also important to clarify this: errors are such an integral part of our programs, and have so much to do with flow control, that I don't believe thinking about errors just as values is enough. Sure, they might be "just values" under the hood, but in all programming languages we see either optional results or syntax sugar to be able to handle errors more gracefully. And in most cases, we still feel it's not enough (or it's enough to be practical, but not to be pleasant in many cases). So we should keep the door open, and not pretend we have already solved errors. Finally, the topic of errors is extremely deep and complex, and when you start introducing other factors like how to report the errors publicly to a non-technical user, maybe in different languages, or whether to log it or send it who knows where, whether to trace or not, how, how to deal with duplicates or similar errors... then you start realizing that we are far from a satisfying and complete model for error handling. We haven't reached this part of the discussion yet. For the moment, passing most errors as values is the relatively painless way that still allows us to customize errors to our needs. But there's still a long way to go. [0] http://joeduffyblog.com/2016/02/07/the-error-model/ http://joeduffyblog.com/2016/02/07/the-error-model/
- mpweiher 5y ago> errors are such an integral part of our programs, and have so much to do with flow control, that I don't believe thinking about errors just as values is enough Exactly. The control flow. Both errors and asynchronous programming share the quality that they don't go well with our call/return based programming model(s). You have to return something, but you either don't have anything (error) or don't have something yet (async). A great solution to this is to use dataflow. This decouples the logic, which is encoded in the dataflow, from the control flow, which just serves to drive the dataflow, and thus negotiable. For async, it is synchrony-agnostic, which is nice, because it solves, or rather sidesteps, the "function colouring" problem. For errors, it allows you to keep error handling out of the happy path without needing exceptions.
- guggle 5y ago"Which program is easier to read?" For me it was the second. Am I the only one ?
- conistonwater 5y agoThe first one reads like somebody just found out about functional programming and functors and tried to "improve" a straightforward bunch of if statements. I can read the second program without having read the plain English description, but I definitely would prefer to have the comment for the first one. I think even in languages like Haskell I would prefer (just sometimes) to read just a straight if-elseif-elseif-else. The whole thing about the way it's written with throwing/catching is a red herring anyway, you should just replace those with a different choice of if's. If you're feeling super adventurous, you can instead replace them with goto's, which is kinda funny; it would actually simplify the code, how often do you see that?
- jamincan 5y agoI think part of the problem is that Typescript doesn't have support for error-as-value baked into the language and pervasive in the ecosystem, so adopting that style isn't as ergonomic as it would be in a language that does. The equivalent in Rust would be far more clear and concise due to the ? operator and Result-types being ubiquitous.
- user-the-name 5y agoThe second by far, especially if you just remove the else statements and let program flow continue naturally. There are very good reasons to prefer Result over exceptions, but this example is not one.
- yakubin 5y agoTbh, to me the first one is easier to read. There's less jumping. But both are terrible. It should just be a bunch of if (...) { ... } else if (...) { ... } else { ...} etc. with no mutation of variables (what are all those v += 1 for?).
- KronisLV 5y agoPersonally, i really like having multiple return values, since being able to give a function multiple inputs but only being able to return a single thing always felt weird - if your require any metadata in a language like Java, then you'd have to come up with wrapper objects and so on. That said, i really dislike the following from the article: if (error) { // you can handle the error as you see fit // you can add more information, end the request, etc. } To me, that's an example of "opt in" error handling, which in my eyes should never be the case. The compiler should force you to handle every exception in some way, or to check for it. My ideal programming language would have no unchecked runtime exceptions of any sort - if accessing a file or something over a network can go wrong in 101 ways, then i'd expect to be informed about these 101 things when i make the dangerous call. Handling those wouldn't necessarily have to be difficult, in the simplest case just wrap it in an implementation specific exception, like InputBufferReadException regardless of whether you're working with a file or network logic and let them bubble upwards to the point where you actually handle them properly in some capacity, be it with retry logic or showing a message to user, or letting external calling code handle it. Why? Because whenever you're given the opportunity to ignore an exception or you're not told about it, someone somewhere will forget or get lazy and as a consequence assumptions will lead to unstable code. If NullPointerExceptions in Java were always forced to be dealt with, we'd either have nullable types be a part of the language that's separate from the non-nullable ones (like C# or TypeScript i think), or we'd see far more usages of Optional<T> instead of stack traces in our logs in prod, because we wouldn't be allowed to compile code like that into an executable otherwise. Of course, that's my subjective take because of my experience and things like the "Null References: The Billion Dollar Mistake": https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/ https://www.infoq.com/presentations/Null-References-The-Bill... I think languages like Zig already work a bit like that: https://ziglang.org/learn/overview/#a-fresh-take-on-error-handling https://ziglang.org/learn/overview/#a-fresh-take-on-error-ha...
- masklinn 5y ago> Personally, i really like having multiple return values, since being able to give a function multiple inputs but only being able to return a single thing always felt weird - if your require any metadata in a language like Java, then you'd have to come up with wrapper objects and so on. MRV is nice and useful, and “error as value” languages usually have ways to return multiple values (usually in the form of tuple), but it’s not proper and correct for error signalling, because the error and non-error are almost always exclusive. In that case, using MRV means you have to synthesise values for the other case (which makes no sense and loses type safety), and that you can still access the “wrong” value of the pair. > To me, that's an example of "opt in" error handling, which in my eyes should never be the case. The compiler should force you to handle every exception in some way, or to check for it. That is what Rust does (including a clear warning if you drop a `Result` without interacting with it at all), although for convenience reasons (because it doesn’t have anonymous enums and / or polymorphic variants) the errors you get tend to be a superset of the effectively possible error set. Though that’s also a factor of the underlying APIs, when you call into libc it can return pretty much any errno, the documentation may not be exhaustive, and the error set can change from system to system. Plus the error set varies depending on the request’s details (a dependency which again may or may not be well documented and evolving). So when you call `open(2)`, you might assume a set of possible errors which is not “everything listed in errno(3) and then some”, but a wrapper probably can not outside of one that’s highly controlled and restricted (and even then it’s probably making assumptions it should not).
- rini17 5y agoTo whomever is going to implement this: Please save the stack trace into the error object at its creation time, at least in debug builds.
- malkia 5y agoOn Windows, there is an API for this - https://docs.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-addvectoredexceptionhandler https://docs.microsoft.com/en-us/windows/win32/api/errhandli... - you can save the callstack there, before a C++ exceptions happens.
- xg15 5y agoNo, I don't want to wrap every single statement of my program in its own if-block, thank you very much.
- simias 5y agoRust solves this issue by having a ? operator to bubble up Errors. Before that there was the try! macro with the same semantics. That cuts the boilerplate to a minimum while having a well defined and explicit control flow. I agree that if you had to write the ifs by hand it would be a pita. Looking at you, Go.
- register 5y agoIn the end that is equivalent to bubble up exceptions when thy are of the unchecked type.
- dthul 5y agoI don't think that's true because if I understand it correctly, the return type of functions which can possibly throw unchecked exceptions would not indicate that they can throw or what they can throw. On the other hand, with the "errors as values" approach (including "bubbling up" operators like `?`), you can tell exactly from the function's return type if an error can be returned and if so what the set of possible errors is. Did you maybe mean "the checked type"? In that case I still think it's not equivalent because at least in Rust you can automatically transform the error while it bubbles up, while I don't know of a language with checked exceptions that lets you transform the exception while unwinding (short of manually catching, transforming, and re-throwing).
- nlitened 5y ago> the return type of functions which can possibly throw unchecked exceptions would not indicate that they can throw or what they can throw As far as I know, that's how Java's "throws" method signature works, which has been widely regarded as a mistake.
- FounderBurr 5y ago“Programming with exceptions is difficult and inelegant” Nonsense.
- yodsanklai 5y ago> Programming with exceptions is difficult and inelegant. Learn how to handle errors better by representing them as values. Funny how exception were invented because handling errors as values was considered to be tedious. And now, more and more languages are going backward.
- tester34 5y agoWell, on the other hand there's difference between handling errors with values like: -1, 0, 1 and other obscure things and using proper types like Result<T>
- deleted 5y ago[deleted]
- jerf 5y agoI think it's less strange than you think. In most languages that use errors as values, the tediousness is being directly attacked instead of trying to dodge around it. Haskell, in many of its uses, cleans up the tediousness so thoroughly that the code written using errors as values can be almost indistinguishable from code written using exceptions, and yet, nevertheless, the errors are values and no exception machinery is being deployed. It has been a general trend in pragmatic programming languages in the past couple of decades. Another huge example, in my opinion, is in typing. Static typing in the 20th century was terrible. Tedious, broken, and missing a lot of its value. So a lot of languages were written that basically amount to a "screw that, we're not using types", and they became very successful. But in the 21st century, a lot of work has been done directly attacking the tediousness and problematic aspects of using static types, while also getting more value out of them with safer languages that more pervasively enforce them and make them more reliable, thus more useful, etc. So we're seeing a resurgance of the popularity of very statically-typed languages... but it's not "moving backwards" because it's not the same thing as it used to be. Much like I don't expect dynamic languages to entirely go away, I wouldn't expect exceptions as we know them to go away either. But I expect "errors as values" to continue attracting more interest over time. In fact, as test34's sibling post sort of observes, there's some synergy between these two trends here. Making strong typing easier has made it easier to have strongly-typed, rich values that can be used as error values and used in various powerful ways. Now that there are languages where it's much easier to declare and fully exploit new types than it used to be, it's much easier to just go ahead and create a new error type as needed for some bit of code without it having to be a big production.
- cletus 5y agoI'm firmly in the camp that believes that exceptions are a false economy. The post links to an "Exception Smells" post that doesn't mention one of my pet peeves: exceptions as control flow. For example, Java's parseInt [1] throws a NumberFormatException if the string can't be parsed. IMHO this is terrible design. As a side note, checked exceptions are terrible design. I wrote C++ with Google's C++ dialect where exceptions were forbidden. Some chafed under this restriction. It was largely a product of the time (ie more than 20 years ago now when this was established). Still there's debate about whether it's even possible to write exception-safe C++ code. In the very least it's difficult. So Google C++ uses a value and error union type, open sourced as absl::StatusOr [2]. The nice thing was you couldn't ignore this. The compiler enforced it. If you really wanted to ignore it, it had to be explicit ie: foo().IgnoreError(); But here's where the author lost me: this chaining coding style he has at the end. To make it "readable" a bunch of functions had to be created. You can't step through that code with a debugger. The error messages may be incomprehensible. I much prefer Rust's or Go's version of this, which is instead imperative. [1]: https://docs.oracle.com/javase/7/docs/api/java/lang/Integer.html#parseInt(java.lang.String) https://docs.oracle.com/javase/7/docs/api/java/lang/Integer.... [2]: https://abseil.io/docs/cpp/guides/status https://abseil.io/docs/cpp/guides/status
- zvrba 5y ago> For example, Java's parseInt [1] throws a NumberFormatException if the string can't be parsed. IMHO this is terrible design. It's unergonomical design, but it's the _correct_ design: the method is declared to return an int, and it can't fulfill its promise: throwing an exception is the right thing to do.
- dthul 5y agoIt's the correct design only if we assume that the design space didn't allow for a different return type. Kotlin for example offers toIntOrNull (https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/to-int-or-null.html https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.text/to-...) as an alternative.
- 5y ago
- zvrba 5y ago> Some errors are unexpected and should stop the program; you want to use exceptions for those. Precisely the opposite: exceptions are a fail-fast mechanism that gives you an alternative to terminating the program. Now, as slx26 mentioned, it's only half of the story. Most APIs (including .NET) document exceptions badly, they're not discoverable, and if you try to use them to _recover_ from a condition, you're in for a world of pain. The workflow is usually: attach the debugger, try to make the exceptional condition happen, inspect relevant info in the debugger and write the catch block. I wish that programming languages supported some contract-like mechanism of declaring: "This method can throw only X, Y and Z". If the method throws anything else, a system-defined "UnexpectedException" would be thrown, encapsulating the invalid one. C++ used this model once upon a time, but they went away from it due to runtime costs and it being little-used. (Also, it terminated the program instead of rewrapping the exception.) Exceptions are first-class values, but few programmers treat them as such, probably because the programming language allows them to do so. So we should start by fixing PLs.
- Joker_vD 5y agoMay I interest you in looking at Java? It has some interesting lessons wrt your proposition.
- p2t2p 5y ago> I wish that programming languages supported some contract-like mechanism of declaring: "This method can throw only X, Y and Z". If the method throws anything else, a system-defined "UnexpectedException" would be thrown, encapsulating the invalid one Boy, do I have some news for you... Like about 25 years old news. Did you ever try java?
- golergka 5y agoThere's a place for both. Error values for conditions that your client will want to handle, and exceptions or panics for all the fatal failures.
- DangitBobby 5y agoI don't really understand the place of panics. It's not really up to a function to determine whether its error is unrecoverable (especially in the case of a library), it's up to the caller. And an unhandled Exception is, practically speaking, a panic. So it seems to me that Exception covers both use cases.
- rowanseymour 5y agoThis took a while for me to get used to coming from Java/Python to Go but I'm very much a convert now - or at least it makes perfect sense for the sorta of Go services we write. It always forces me to think, can this thing fail in normal operation or is this exceptional. If former, it's an error value that eventually should be returned to the client in some form. If latter, it's a panic and I'll see it in Sentry and know I probably have something to fix.
- XVincentX 5y agoOn the same topic: When An Error Is Not An exception Series: https://dev.to/vncz/series/6223 https://dev.to/vncz/series/6223
- deleted 5y ago[deleted]
- 3r8Oltr0ziouVDM 5y ago>Only throw exceptions when something really bad has happened and the program must stop. For example: > the program cannot connect to its database; > the program cannot write output to disk because the disk is full; > the program was not started with valid configuration. I'd prefer Result over exceptions even in these cases. The only case where I think exceptions should be used is when the type system of the language is not powerful enough to prove the validity of some operation. For example in Rust: let v = vec![1, 2, 3]; let n = v.pop().unwrap(); The `pop` method returns `Option`, but I as a programmer know for sure that the collection isn't empty, I just can't prove it to the compiler. So I use `unwrap` to get the value and panic in the case I'm actually wrong and made a stupid mistake. Another example is division by zero. Using a `Result` as a return value of the division operator would be extremely inefficient and unergonomic. Panic/exception is the best way to handle this situation. I believe dependent types can solve both of the problems above so we can get rid of exceptions completely. Unfortunately, there is no a single mainstream language that has them.
- omegalulw 5y agoAnother thing to call out is that you also need to be precise in what the error is. Division by zero is indeed a grave error, but only when your code is not logically thought out - there should never have been any codepaths that divide by zero in the first place. So the error that your should report is whatever cause the denominator to be zero, not division by zero. That's almost entirely useless, and misleading.
- Jonathanks 5y agoYes, this. Parse all input at the application boundaries and reject invalid input. For division by zero, the code path that leads to that should encode the input as a number greater than zero. But this may be clunky to do, depending on the language you're working with.
- FpUser 5y agoErrors as values are fine and useful. However author also says this: "Programming with exceptions is difficult and inelegant." I am of completely opposite opinion: to me exceptions are very easy to use and elegant for what they intended. It does not mean that one has to rely only on exceptions or on plain error as values. Use both for the best benefits depending on situations. Why programmers get obsessed doing thing in "there can be only one right and true tool language, concept, style etc. etc." way is completely beyond my understanding.
- deleted 5y ago[deleted]
- oftenwrong 5y agoI agree that both exceptions and error values (aka result types) have their place. I would say that error values are good for when a caller should explicitly handle that case, and that exceptions are good for errors that a caller should not be expected to handle explicitly. A lot of times this breaks down as meaningful application errors vs operational or programming errors. I am struggling to find the right words for this, so I can give an example: Let's say we have a function used to register a new user account on a site like HN. An error value would be appropriate to return when the username is already taken, so that we can express to the caller that this is a possibility that must be handled. Most likely the caller would want to tell the user. A maintainer doesn't really care when this occurs, since it's part of the application's healthy behaviour. An exception would be appropriate if the database is unavailable. The caller would not be expected to tell this to the user, nor is there any logical way for the caller to react to this situation specifically. In this example of a web app, the best course of action is likely returning a generic "unexpected error" message and/or a HTTP 500. The caller can typically let the exception propagate to the web layer's top level exception handler where it will be logged. As a maintainer of the system, a stacktrace is valuable for pinpointing the problem with the code path that lead to it. (Checked exceptions, where available, blur these lines a bit) --- In the Java world... (stop reading if you don't care about Java) ...it has been increasingly common to see types like Result<T,E> used for error values. Recently, there have also been additions to the language that make errors-as-values more practical. Sealed classes (a preview feature in Java 16, and a full feature in the soon-to-be-release Java 17) are basically an implementation of product types (with a characteristically verbose Java-ey syntax) that could be used to implement results. Returning to our example with this: sealed interface RegistrationResult { record Registered(Account newAccount) implements RegistrationResult { } record UsernameTaken() implements RegistrationResult { } ... } https://openjdk.java.net/jeps/409 https://openjdk.java.net/jeps/409 beyond Java 17, you will be able to pattern-match over these with exhaustiveness enforced by the compiler. It will look something like: switch(registrationResult) { case Registered(Account newAccount) -> ...; case UserNameTaken() -> ... ; ... } https://openjdk.java.net/jeps/405 https://openjdk.java.net/jeps/405
- _448 5y agoI am writing some C++ code for a web application, and there I am handling errors via exception. There are two broad types of exceptions, one that is internal and one that needs to be reported to the user. Following is how I an handling the errors, please could you all suggest a better approach if my approach is sub-optimal designwise? // Base class HandleRequest(req, res) { try { try { post_processing(req, res) // implemented by derived class process_request(req, res) // implemented by derived class pre_processing(req, res) // implemented by derived class } catch(send_to_user_exception) { send_error_to_user(send_to_user_exception.what()) // implemented by derived class } } catch(internal_exception) { log_error(internal_exception.what()) send_internal_error_to_user(internal_exception.what()) // implemented by derived class } catch(unknown_exception) { log_error(unkown_exception.what()) send_internal_error_to_user(unkown_exception.what()) // implemented by derived class } } // Each request type is handled by its corresponding derived class and implements the following methods of the base class. post_processing(req, res) // will throw exceptions of type send_to_user_exception and internal_exception process_request(req, res) // will throw exceptions of type send_to_user_exception and internal_exception pre_processing(req, res) // will throw exceptions of type send_to_user_exception and internal_exception send_error_to_user(error) send_internal_error_to_user(error)
- blub 5y agoThere's nothing wrong design-wise with your approach, IMO. I've seen several people (including very well-known C++ personalities) argue that exceptions should be used for X and error codes for Y, but this is just convention. C++-wise, you probably want to catch std::exception and "..." too. Finally, you said that there's two types of exceptions and only one of them is supposed to be reported to the user, but in your code you seem to report everything to the user. You should edit your message to clarify what you meant.
- _448 5y ago> C++-wise, you probably want to catch std::exception and "..." too. Yeah, the "unknown_exception" in the above pseudocode represents that :) > Finally, you said that there's two types of exceptions and only one of them is supposed to be reported to the user, but in your code you seem to report everything to the user. You should edit your message to clarify what you meant. Yeah, only one will be reported because only one exception handler will be called. So the internal error will be reported to the user as "internal error" and some internal code that the user can report back to me if they want to. The other error is user error. So broadly there are only two categories of errors.
- layer8 5y agoIn my opinion, the difference between errors as return values and checked exceptions is merely one of syntactic sugar. Both are conceptually a sum type together with the regular return value, and the syntactic sugar for handling and/or propagating the error or exception is really a spectrum, not a dichotomy. I believe it would be useful to focus on the possible design choices within that spectrum, regardless of the underlying implementation mechanism. Of course, the implementation mechanism matters at the machine code level or in the runtime. However, that is mostly a question of performance trade-offs and interoperability, but otherwise just an implementation detail, and doesn’t have to be a question of expressiveness and code-level semantics. You can implement exceptions as subroutine return values, and you can implement error return values with exception-like mechanisms behind the scenes. That should be a different concern from how it looks like at the source-code level.
- Fellshard 5y agoThe main caveat is that oftentimes, checked exception handling doesn't compose well - see what kind of trouble Java gives you, for example. Recent articles I've read on effect modeling languages seems to give a more uniform construct for bringing checked exceptions in line with other control constructs.
- layer8 5y ago> see what kind of trouble Java gives you, for example. I program a lot in Java, and the only trouble there is the lack of support for sum types and/or variadic type parameters in generics (i.e. to express functional interfaces that can throw an arbitrary number of checked exceptions, as a type parameter). That’s the only pain point for me and is something that could be fixed. In fact, the interplay with control structures is exactly what I was referring to by syntactic sugar. Indeed it also concerns the type system. But let’s talk about that, not about exceptions good/bad or error values good/bad. That’s too simplisitic.
- deleted 5y ago[deleted]
- agent327 5y agoThat second program must be the single worst example of exceptions ever written; a straw man if ever these was one. The key in understanding why this is the case lies in the realisation that ParseInt is a combined parser/validator, and a validation failure isn't actually an error; it's a normal, expected, situation. In C++, you'd solve this by returning std::optional<int>, and end up with code like this: std::optional<int> result = ParseInt (input, 8); if (!result) result = ParseInt (input); if (!result) result = ParseInt (input, 16); if (!result) throw ...; return *result; Note how there's no exceptions (in ParseInt, I mean). Note how there's no error codes either. There's just no error handling needed to begin with, except right at the end, if the number is not in any of the three supported formats.
- Zababa 5y ago> There's just no error handling needed to begin with, except right at the end, if the number is not in any of the three supported formats. I would argue that if (!result) is a form of error handling, as result being falsy indicates that the parsing failed.
- DangitBobby 5y agoThe way I see it, the issues with Exceptions are with 1) types and 2) the try/catch syntax, and the issue with Error-As-A-Value is that it's cumbersome. Exceptions solve a very real problem. Sometimes I get to the point where there's nothing more I can do and it's time to start unwinding the stack. I eventually either signal with try/catch that I'm ready to start handling the issue somewhere up the stack or never do and I crash. Error-As-A-Value addresses the "types" problem (specifically Option types do this; Go ignores this problem AFAIK and errors are poorly supported by the type system) and "forces" users to be explicit, except they can always just ignore the value when they need to anyway but now with added boilerplate. Just as importantly, they propagate this boilerplate to any caller, even if the caller doesn't care. Having to say within each and every caller, no, I really don't care about this error and there's nothing I can do about it right now is tedious, cumbersome, and often truly introduces no value. I think we can do better than either by allowing the use of both. What if I had the compiler and other tooling keep track of the Exceptions that can be thrown? const RandomError = new Error("you have bad luck!") const DivideByZero = new Error("cannot divide by zero!") // this can only throw RandomError const maybeAdd = (a: number, b: number): number => { if (randrange(0, 1) > 0.5) throw RandomError return a + b } ?? RandomError // myFun can throw RandomError or DivideByZero, and our tooling // will help us keep track of that. const myFun = (a: number, b: number): number => { if (b === 0) { throw DivideByZero } return maybeAdd(a, b) / b } ?? DivideByZero Well, in TS/JS, now I still need to use try/catch at some point to handle the exceptions this will eventually throw. But maybe an error-as-a-value makes more sense. What if I included sugar that optionally replaced try/catch with error-as-a-value, if that's what the use case called for? type Result = { ok: number error: DivideByZero | RandomError } // myFun(1, 2)? will return the result type indicated above let { ok, error } = myFun(1, 2)? while (!ok) { { ok, error } = myFun(1, 2)? } return ok This is an unfortunately contrived example but I think it demonstrates my point. I don't really see any reason we can't have both in modern languages. 1. The problem with "types" in Exceptions being that you usually don't have any insight into whether or what errors can be thrown in a language that uses Exceptions as the main error handling control flow 2. The problem with try/catch syntax is subjective, but sometimes you don't want to introduce new scopes and at least 4 new lines. And code with extensive error handling becomes unnecessarily littered with try/catch when you would have preferred an abbreviated assignment expression as with error-as-a-value.