[HN Gopher] Away from Exceptions: Errors as Values
___________________________________________________________________
Away from Exceptions: Errors as Values
Author : Jonathanks
Score : 88 points
Date : 2021-08-28 10:54 UTC (2 days ago)
(HTM) web link (humanlytyped.hashnode.dev)
(TXT) w3m dump (humanlytyped.hashnode.dev)
| _448 wrote:
| I 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 wrote:
| There'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 wrote:
| > 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.
| rowanseymour wrote:
| This 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.
| xg15 wrote:
| No, I don't want to wrap every single statement of my program in
| its own if-block, thank you very much.
| 0xFEE1DEAD wrote:
| It's a trade I'm willing to take for simplifying reasoning
| about non-happy-paths. Try catch is great in theory but it's
| super easy to shoot yourself in the foot with in bigger
| projects, either because it's non exhaustive or someone took
| the easy way out and made a catch which isn't fine grained
| anough.
| simias wrote:
| Rust 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 wrote:
| In the end that is equivalent to bubble up exceptions when
| thy are of the unchecked type.
| simias wrote:
| The key difference for me is that you have to explicitly
| handle Results somehow, you can't just pretend that the
| function is infallible and hope that something up-stack is
| going to deal with all the failure modes. Also while you
| can have generic Error types it's generally frowned upon
| for libraries which are encouraged to provide meaningful
| error types that can be used to decide how a problem should
| be dealt with. Coupled with Rust's pattern matching it
| makes for concise and expressive error handling in my
| experience.
| dthul wrote:
| I 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 wrote:
| > 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.
| barbarbar wrote:
| Throws is for checked exceptions. Unchecked are not
| listed in the throws list.
| oftenwrong wrote:
| It's actually even worse: you can include both checked
| and unchecked exceptions in the `throws` clause. The
| compiler will only enforce handling of the checked
| exceptions included. Unchecked exceptions listed in the
| `throws` clause serve as an optional hint to others. Note
| that you can erroneously include any exceptions you like
| in the `throws` clause, even ones that are never thrown
| from the method. These quirks are often covered by static
| analysis.
| wvenable wrote:
| One could solve the problem further and more conveniently by
| doing the bubble up implicitly at every call and just re-
| invent exceptions.
| jstimpfle wrote:
| If you have to do this, the structure of your program might be
| wrong.
|
| Don't check the return value of a call to the same API multiple
| times. Make it such that all calls to the API go through the
| same code that you write, so you have to check the return value
| only "once". This may sound extreme, but it's pretty close to
| what you can realistically achieve.
|
| You can achieve that trivially by exiting if something goes
| wrong when calling the API. And where exiting is not possible
| because it's a longer running application, concerns have to be
| separated: If there are multiple pieces of code that need the
| same data, feed those pieces the data they need from a central
| location that interfaces with the API. And have the error
| handling logic (which is usually higher level control logic) in
| the central location.
| tester34 wrote:
| Have you considered using patterns that help dealing with that?
|
| like Railway Oriented Programming
|
| https://www.youtube.com/watch?v=45yk2nuRjj8
|
| I believe that almost every thing should return Result<T>,
| because almost everything can fail and compiler should scream
| when you do not handle those fail pathes.
|
| That makes me believe that C#'s "FirstOrDefault" for value
| types sucks, because you're never sure whether the "Default"
| comes due to lack of value or because the found value is
| actually the same as default
|
| e.g
|
| var list = new List<int> {1,2,3};
|
| var found = list.FirstOrDefault(x => x < 1);
|
| found = 0 (default)
|
| meanwhile 0 may be valid value! so we aren't sure whether it is
| error or an actual value
|
| and we have to perform e.g casts to `int?` or stuff to detect
| that.
|
| using Result<T> gives very precise information
| Sebb767 wrote:
| > meanwhile 0 may be valid value! so we aren't sure whether
| it is error or an actual value
|
| This function is useful when you don't really care whether
| it's an error or the value. Imagine you're querying the view
| count of an item of the user. If the user is anonymous, the
| select might give an empty result, but you only care about
| showing a number to the user. So 0 is absolutely fine in that
| case.
|
| If it's important to you whether the item actually exist,
| you're using the method in the wrong place.
| tester34 wrote:
| Yes, I can always use First and catch Exception, which is
| meh.
|
| First returning Result<T> or something like FirstOrNull
| would be better, because FoN would work for ref types -
| classes and stuff the same way (afaik) as FoD does, but
| it'd make error handling for value types like ints more
| precise
| cube00 wrote:
| This was bread and butter error handling in COM, everything old
| is new again!
| pjmlp wrote:
| Still is, for those that never discovered C++/CX, WIL or
| C++/WinRT and are stuck in the ways of Windows XP.
|
| https://docs.microsoft.com/en-us/cpp/cppcx/exceptions-c-
| cx?v...
|
| https://github.com/microsoft/wil/blob/5a21cac10640f54b7ef886.
| ..
|
| https://docs.microsoft.com/en-us/windows/uwp/cpp-and-
| winrt-a...
| Joker_vD wrote:
| Yep, all hail the mighty #define
| CHECK(com_expr) hr = (com_expr); if (FAILED(hr)) { return hr;
| }
|
| Ah, the memories... glad I don't have to touch it ever again.
| "And a million other things that, basically, only Don Box
| ever understood, and even Don Box can't bear to look at them
| any more".
| pjmlp wrote:
| COM is where all new OS APIs land nowadays, since Vista.
|
| Even if they are eventually wrapped in .NET libraries.
| ewg4345h43 wrote:
| It 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.
| agumonkey wrote:
| It was a very conscious decision by go devs. To enforce local
| error fixes instead of the usual wrap and throw the exception
| again.
| [deleted]
| feffe wrote:
| The article is not about Go, although that was my guess before
| reading it as well.
| iwwr wrote:
| You 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 wrote:
| You 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 wrote:
| You 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]
| tsimionescu wrote:
| Realistic 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 wrote:
| > 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??>
| [deleted]
| agent327 wrote:
| That 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 wrote:
| > 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 wrote:
| The 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.
| praptak wrote:
| To 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.
| 3r8Oltr0ziouVDM wrote:
| >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 wrote:
| Another 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 wrote:
| Yes, 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.
| [deleted]
| FounderBurr wrote:
| "Programming with exceptions is difficult and inelegant"
|
| Nonsense.
| koblas wrote:
| Interesting 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 wrote:
| Personally, 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 wrote:
| suretype 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 wrote:
| It'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/
| pengstrom wrote:
| We can't forget that "error handling" is a civilization-level
| hard problem. Do you set up support structures to catch them
| when shit hits the fan, or do do you preventatively and
| excruciatingly suppress them?
|
| It's as much a theoretical problem of what errors are as a
| practical problem in how to represent these intricate models
| ergonomically, and what will be sacrificed. In some sense it's
| an almost moral question!
| swader999 wrote:
| And then throw PII concerns into the fray. How do you report
| meaningful information to your ops team but still not expose
| PII in your logs. There's ways of course but its not trivial.
| enumjorge wrote:
| > 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...
|
| I've tried searching for articles that talk about people deal
| with this in the context of web apps but have found it
| difficult to find content. It's a tricky topic to google. Most
| of what I find are (usually content marketing) articles about
| how to log errors and/or how to send them to some service.
|
| I'll give you an example that is admittedly a little paranoid.
| Take a switch case statement where you branch of an enum-like
| value, meaning there's a set of known values you expect. What
| do you do for the default case? In theory you don't need a
| default case because you "know" the switch won't hit it, but
| it's weird to me to write code that has no logic to handle a
| possible scenario even if it's highly unlikely. What if there's
| a bug in the code or the enum-like value changes to contain a
| new value or some weird edge case? The point being in
| JavaScript there's no way to be 100% sure that the switch
| condition will not contain an unexpected value.
|
| Given how unlikely this scenario is though, how do you deal
| with it? (I realize some of it depends on where in the
| application's code this is happening in). You don't want to
| throw an exception and break the app. Or you can but you'd want
| to catch at some point. Do you log the event in the backend and
| create an alert so that you know a user ran into a weird edge
| case or bug? Do you write it out to the console in case you get
| a customer support call so that you can identify the issue? Is
| it a bad idea to write out errors like that to the console?
|
| I'm sure these are questions that most mature apps have had to
| answer, but I haven't been able to find what people consider
| best practices for these types of situations. If anyone knows
| of good resources I'd love to read them.
| slx26 wrote:
| I recommend again reading the article I linked to. The answer
| to the first part is: this should be an exception (or
| abandonment, as the Midori team called it). This is an error
| in the logic of the code, it's a programming error (even if
| it's due to later changes or whatever). It's an error that
| needs to be fixed in the code, not "recovered from".
|
| Now, you can also catch exceptions, indeed. You could have
| your app catch all exceptions at the root level or wherever
| you think it's appropriate if your code is modular enough.
| Once you have caught the exception, you could silence it, as
| a lot of software does, and pray for the best... or be a bit
| more serious. If it was me, I would notify the user: "There
| has been an unexpected error". I would also append the
| technical info in a "technical details" section or something.
| And I would also provide a link to let the user report the
| issue easily. I'm kinda against hidden automatic reports for
| privacy-related reasons, as errors might sometimes contain
| sensitive data too, but it would really depend on the
| application.
|
| There are many ways to make this more robust. Check for
| report dups on your side, or have some dynamic code to check
| the status of a specific error to provide the user with even
| more information, or even silence the error completely or
| whatever. But all this takes much more work, and it's really
| dependent on the application you are writing and how
| entreprisey you are willing to go. Crashes are not nice, data
| loss is not nice, but neither is corrupted data or subtle
| bugs due to errors silenced for the sake of the peace of mind
| of your users. You have to decide what's the right balance
| based on the type of program you are writing. Indie
| videogame? Crash as soon as possible, ask nicely for reports,
| get bugs fixed fast. Editor where a lot of data might be lost
| if you are lousy with exceptions? Definitely go out your way
| to auto-save separately if possible before crashing, and let
| the user know how to try to recover its data and how to get
| assistance. Non-critical webapp? Just let the user know
| something unexpected has happened, and allow to report and
| assure you will look into it soon. It always depends.
|
| EDIT: I missed the most critical part, so I'll add it now...
| When communicating errors to users, the most important part
| is properly handling their frustration, not the logging
| method or the technical details included or anything else. If
| you have "few users", make sure they have a way to get in
| touch, and make sure they get a fix or a decent explanation
| of what's going on, let them know when it will be solved or
| what can they do meanwhile. Errors happen, but people are
| most often very understanding as long as you are there and
| don't leave them alone with their frustration. If you have
| too many users for that... good luck to you.
| Jonathanks wrote:
| Author here. Thank you for the link to Midori's error model.
| It's on my reading list for this week.
|
| > So we should keep the door open, and not pretend we have
| already solved errors.
|
| Very much this. Even with errors as values, the approach
| languages like Go take makes composition difficult. I presented
| the Kleisli approach instead. That recovers compositionality.
| OCaml does something I find interesting. It makes exceptions
| performant by not capturing stack traces by default. But it's
| still too easy to forget handling the exception.
|
| > btw, the article uses Rust, not Go, but anyway...
|
| I actually used TypeScript. The Rust bit was meant to introduce
| the idea from Rust to TypeScript. It's not new in TypeScript,
| but it's not popular either. I have updated the article to
| clarify that.
| mpweiher wrote:
| > 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.
| ithkuil wrote:
| Interesting. Can you give some pointers/examples?
| yodsanklai wrote:
| > 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.
| [deleted]
| jerf wrote:
| I 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.
| dkarl wrote:
| > 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.
|
| I don't have experience with Haskell, but I have mixed
| feelings about monadic error handling in Scala for precisely
| this reason. It goes to great lengths to recreate the
| programming ergonomics of exceptions, with exactly the same
| drawbacks. Monadic error handling, aka "railway-oriented
| programming,"[0] splits your logic into two tracks: a "good"
| track, where all your happy path logic lives, and a "bad"
| track, which is automatically propagated alongside your happy
| path logic. In my experience, it induces the same programmer
| mistakes as exceptions do: errors get accidentally swallowed
| (especially where effects are constructed and transformed,)
| different errors that require different handling are
| accidentally treated the same, and programmers fall into the
| habit of seeing the error track as an inferior, second-class
| branch compared to the happy path.
|
| It confuses me when programmers (not talking about you,
| because I don't know how you write code, but people I've
| worked with personally) bash exceptions and then use monadic
| error handling to achieve exactly the same trade-offs.
|
| This hasn't turned me off of monadic error handling, but it
| has made me think of it as FP's version of exceptions, rather
| than an upgrade. Personally, I think exceptions are a good
| enough trade-off in most cases, but when you need to be more
| careful, it is better practice to give all paths the same
| prominence in code. FP provides a better way to do this:
| pattern matching. More verbose, yes; harder to spot the happy
| path when reading code, yes; encourages more careful and
| thorough thinking about errors, for me absolutely yes. YMMV.
|
| [0] https://fsharpforfunandprofit.com/rop/
| munchbunny wrote:
| > This hasn't turned me off of monadic error handling, but
| it has made me think of it as FP's version of exceptions,
| rather than an upgrade.
|
| This reminds me a lot of Java's checked exceptions just
| with different window dressing. You move the failure mode
| type information from the exceptions list ("throws" clause)
| into the return type.
|
| Typed error return values is definitely an improvement in
| ergonomics over C-style error code returns, and pattern
| matching is definitely a big improvement on ergonomics too,
| but I think error handling has a problem of fundamentally
| irreducible complexity. For example, if you make network
| calls, you have to be prepared for network calls to fail,
| and you have to design your system to recover from it
| somehow, whether that happens in a try/catch or a match on
| Either[Throwable, Result].
| jerf wrote:
| I've started writing a blog post that covers this topic,
| and once I started thinking clearly about it, I've found
| errors to be a really hard problem.
|
| To even get out of static vs. dynamic typing, I've
| expressed the problem as "Given this piece of code, how
| can I know what types of errors will come out of it?",
| where I'm using a human, loosey-goosey sense of the word
| "type" here, rather than necessary a strict type. (If
| your language wants to answer that in terms of strict
| types, great, but I'm trying to answer it very generally
| across programming languages.) It turns out that from
| what I can see, the underlying problem is that "errors
| don't compose"; given a function f that returns errors X,
| Y, and Z and accepts another function f2 to call that may
| return other errors, it is really difficult to
| characterize f(f2) in practice. For a concrete, static f2
| we can mostly at least imagine taking the union of the
| two (although even that can be an oversimplification;
| what if f calls f2 in such a way that one of the types of
| errors that f2 can produce is guaranteed not to happen,
| e.g., what if f2 could throw a null pointer exception but
| it can be easily statically proved that f never passes it
| one?), but once you let enough polymorphism into the mix
| to let f2 be an arbitrary closure of some type, all bets
| are off in terms of what f(f2) can produce in most
| languages.
|
| This hurts statically-typed languages in that they can't
| create very strong types for these sorts of situations,
| but more generally, translating the theory up to
| practical experience regardless of the language being
| used, A: it's hard to program in an environment where you
| have enough polymorphism of some sort (OO, accepting
| closures, whatever) to have errors mix like this in your
| code and _know_ what sort of errors may occur where and
| B: that 's nearly everything because programming in an
| environment that lacks that polymorphism is not something
| we generally do voluntarily. (Embedded code not allowed
| to even allocate on the stack is this static, but we
| can't build everything that way.)
|
| Amusingly, I think what saves us in the end is that to a
| first approximation, _there is no such thing as error
| handling_. All there is is logging something for a human
| and giving up. Obviously, to a second approximation there
| is a such thing as error handling. I 've got plenty of
| it. But honestly, it's pretty rare by percentage. Most
| errors result in a log message and some level of failed
| task. Fortunately, with some work, we can usually get our
| systems fed enough good data that we can do things
| without errors, such that the ones that do occur end up
| almost always being essentially correctly handled by
| screaming and dying. If programming actually required us
| to _handle errors_ , like, in some intelligent manner all
| the time, we'd have a lot fewer programs in the world!
| tester34 wrote:
| Well, 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>
| yccs27 wrote:
| Yes, errors-as-values only works well with a type system which
| supports discriminated unions. And programming languages with
| such support have only recently become popular.
| yodsanklai wrote:
| OCaml is a somewhat old language with algebraic types (so
| including discriminated unions) yet exceptions were
| introduced precisely to avoid dealing with propagating errors
| as a values. I agree with your point, but would argue that
| even with sum types, propagating errors is tedious and
| there's a case for using exception.
|
| I wonder if another reason why errors as value are making a
| come back is because of the asynchronous programming style
| which is becoming quite pervasive, and doesn't play well with
| exceptions.
| guggle wrote:
| "Which program is easier to read?"
|
| For me it was the second. Am I the only one ?
| yakubin wrote:
| Tbh, 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?).
| Joker_vD wrote:
| Those v += 1 implement the exact specification given above:
| 1. Parse an integer N from a string. 2. If N is NaN,
| fail with an error. Otherwise, increment N by 1. 3.
| If N is > 3, fail with an error. Otherwise, increment N by 1.
| 4. If steps 1-3 failed, set N to 3. 5. Increment N by
| 1.
|
| Those are quite strange requirements, and the resulting
| second code looks strange too, but... it faithfully and
| obviously correctly represents the given specification.
| Jonathanks wrote:
| The example is strange and contrived. I couldn't torture
| the logic enough to demonstrate how readability breaks down
| with exceptions. Reading through the comments, I realise
| that even if the code is unreadable with exceptions, it can
| be rewritten to be readable. I updated the article to
| highlight not only readability, but also compositionality.
| I also tried to draw parallels with Promises (which are
| also monadic). I tried to target the article to
| intermediate programmers that may not already know most of
| the concerns raised in the comments, and I surely can't
| cover enough space on error handling without writing a
| book.
|
| Thank you for the comments. I've learned more from reading
| the comments.
| Cthulhu_ wrote:
| It's the kinda assignment where you should look at a higher
| level - what does it do? What is N used for? What are the
| possible inputs?
|
| I mean if you start with a set of tests instead of
| pseudocode written down in text you could probably write
| something smarter.
| mschuetz wrote:
| Second one is way easier to read, even though it's verbose on
| purpose.
| alfonsodev wrote:
| Agree and it could be easier to read in my opinion if you deal
| with the exceptions first. For example. let v
| = Number.parseInt("a3", 10); try { if
| (Number.isNaN(v)) { throw new Error("NaN");
| } else if (v > 3) { throw new Error("gt 3");
| } v += 1; } catch (error) { v =
| 3; } v += 1;
|
| Or writing a "guard function" that throws ...
| function throwIfNaNorGt3(v) { if (Number.isNaN(v))
| { throw new Error("NaN"); } else if
| (v > 3) { throw new Error("gt 3");
| } } let v = Number.parseInt("a3", 10);
| try { throwIfNaNorGt3(v); v += 1;
| } catch (error) { v = 3; } v += 1;
| conistonwater wrote:
| The 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 wrote:
| I 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 wrote:
| The 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.
| FpUser wrote:
| Errors 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.
| KronisLV wrote:
| 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.
|
| 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-Bill...
|
| I think languages like Zig already work a bit like that:
| https://ziglang.org/learn/overview/#a-fresh-take-on-error-ha...
| masklinn wrote:
| > 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).
| tialaramex wrote:
| Does a panic count as "handling" the error?
|
| I actually agree with Rust's choice here. You, the
| programmer, know whether some particular error is something
| you can cope with or not and it's appropriate to panic in the
| latter case. Where you draw the line is up to you, in a ten
| line demo chances are "the file doesn't exist" is a panic, in
| your operating system kernel maybe even "the RAM module with
| that data in it physically went away" is just a condition to
| cope with and carry on.
|
| My litmus test here is Authenticated Encryption. The obvious
| and easy design of the decrypt() method for your encryption
| should make it _impossible_ for a merely careless or
| incompetent programmer to process an unauthenticated
| decryption of the ciphertext. This makes most sense if you
| have an AE cipher mode, but it was _already_ the correct
| design for both MAC-then-Encrypt or Encrypt-then-MAC years
| ago, and yet it 's common to see APIs that didn't behave this
| way especially on languages with poor error handling.
|
| In languages with a Sum type Result like Rust, obviously the
| plaintext is only inside the Ok Result, and so if the Result
| is an Err you don't have a plaintext to mistakenly process.
|
| In languages with a Product type or Tuple returns like Go,
| it's still easy to do this correctly, but now it's also easy
| to mistakenly fill out the plaintext in the error case, and
| your user may never check the error. Dangerous
| implementations can thus happen by mistake.
|
| In languages with C-style simple returns, it's hard to do
| this properly, you're likely using an out-buffer pointer as a
| parameter, and your user might not check the error return.
| You need to explicitly clear or poison the buffer on error
| and even then you're not guaranteed to avoid trouble.
|
| In languages with Exceptions, the good news is that the
| processing of the bogus plaintext probably doesn't happen,
| but the bad news is that you're likely now in a poorly tested
| codepath that isn't otherwise taken, maybe far from the
| proximate cause of the trouble. Or worse, your user wraps
| your annoying Exception-triggering decrypt method and repeats
| one of the above mistakes since they don't have better
| options.
| Joker_vD wrote:
| > Does a panic count as "handling" the error?
|
| > You, the programmer, know whether some particular error
| is something you can cope with or not and it's appropriate
| to panic in the latter case.
|
| Please don't. I've seen enough libraries whose authors had
| exactly this mindset; I do _not_ enjoy when some fifth-
| party dependency thrice-removed, upon encountering an
| unexpected circumstance, decides that it can 't bear to
| live in this cruel world any more and calls "abort()",
| killing the entire process: which happens to be a server
| process, running multiple requests in parallel for which a
| failure to serve any single request for any reason
| _whatsoever_ does not warrant aborting all other request.
| masklinn wrote:
| > Does a panic count as "handling" the error?
|
| Undeniably? Fundamentally the language proposes, the
| developer disposes[0] and short of Rust being a total
| language, panics were going to be a thing.
|
| So while one can argue that the ability to panic should not
| be so prominent, it's certainly an error handling strategy
| which was going to be used anyway, is perfectly valid (in
| some situations), and is convenient when you're designing
| or messing around.
|
| Hell, even _ignoring_ an error is a perfectly valid
| handling strategy, and indeed pretty easy to implement,
| just... explicit (though not the most visible sadly, it 's
| much harder to grep a `Result` being ignored than one being
| unwrapped or expect-ed).
|
| The important bit is that Rust warns you about the error
| condition(s), and lets you decode on how to handle it.
|
| [0] though there are panicing Rust APIs where it doesn't
| just propose
| hypertele-Xii wrote:
| > The compiler should force you to handle every exception in
| some way, or to check for it.
|
| This is the single most unproductive mis-feature a language
| could have for me. Programming is already a tedious excercise
| of wrangling your thoughts into an alien form the computer can
| understand. You want, on top of everything else, the computer
| to _refuse_ to run your program at all, unless you _explicitly_
| handle _every possible edge case?_
|
| I get that some people are engineers with rigid requirements.
| I'm an artist - I _sculpt_ the program to produce output I 'm
| not entirely clear on. I'm _trying_ to make the computer to
| interesting, unexpected things.
|
| Say I'm making a game. I wanna load a character sprite from an
| image file and draw it on the screen. Do I _really_ need to
| handle all the possible ways that file could fail to load
| _right now,_ before even seeing a _preview_ of what it _should_
| look like? Hell no!
|
| It's like having an assistant who refuses to do anything unless
| you specify everything! Hey assistant, get me a coffee. "I
| refuse to get you a coffee because you didn't specify what I
| should do in case the coffee machine is broken." Aargh!
| KronisLV wrote:
| > You want, on top of everything else, the computer to refuse
| to run your program at all, unless you explicitly handle
| every possible edge case?
|
| Precisely!
|
| Even better - let the IDE suggest to you all of the possible
| exceptions and when you're feeling lazy or are hacking away
| at a prototype, either let it add a "throws SomeException" to
| the method signature and make it someone else's problem up
| the call chain, or just add a catch all after you've handled
| the ones that you did want to handle!
|
| After all, none of us can recall the hundreds of ways network
| calls can get screwed up, but we're pretty sure what to do at
| least in a subset of those, but we'd also forget about those
| without these reminders. Not only that, but when you're
| writing financial code or running your own SaaS, you'll at
| the very least will want your error handling code to be as
| bulletproof as the guarantees offered to you by your
| language's rigid type systems.
|
| Then, when you've finished hacking together your logic, your
| instance of SonarQube or another tool could just tell you:
| "Hey, there are 43 places in your code where you have used
| logic to catch multiple exceptions" and then you could review
| those to decide whether further work is necessary, or whether
| you can add a linter ignore comment to the code explaining
| why you don't want to handle the edge cases, or just do so in
| the static code analysis tool, so all of your team members
| know what's up.
|
| Alternatively, if you're just writing something for yourself,
| just leave it as it is, knowing that if you'll ever need to
| publish your code for thousands of others to use, then you
| probably should go back to those now very visible places and
| review it.
|
| So essentially: /** * Attempts to
| load a Sprite from a file. You can then use the instance to
| display it on screen. * @param file This is the file
| that we want to load the image from. Use relative path to
| "res" directory. * Our engine loads PNG files and
| technically can also load GIF files because someone hacked
| that functionality together in an evening. * That's
| kind of slow though, so we should use PNGs whenever possible.
| See ENGINE-33452 for more details. * @return A Sprite
| instance that you can pass to the rendering logic to put it
| on the screen, or alternatively process the loaded image in
| memory. */ public Sprite loadSprite(@NotNull
| File file) throws SpriteGenericException,
| FileSystemGenericException { try { return
| FileSystemSpriteLoader.loadPNG(file); } catch
| (ImageWrongFormatException e) {
| wrongImageFormatLogger.warn("We found a " +
| e.getActualFormat() + " format file: " + file.getPath(), e);
| // the art team should have a look at this if
| (e.getActualFormat().equals(ImageFormats.GIF)) {
| return FileSystemSpriteLoader.loadGIF(file); // TODO
| unoptimized call because we needed GIFs for ENGINE-33452,
| remove later } else { throw
| SpriteGenericException("We failed to load sprite from file: "
| + file.getPath() + " because of wrong format: " +
| e.getActualFormat(), e); } } catch
| (SpriteCorruptedException e) {
| brokenImageLogger.warn("We found a corrupted sprite in file:
| " + file.getPath(), e); // maybe the pipeline is broken
| again? throw SpriteGenericException("We failed to
| load sprite from file: " + file.getPath() + " because of
| image corruption", e); } catch (Exception e) { //
| TODO ENGINE-44551 handle the file system access cases later
| once the API is stable and we know how it'll work on Android
| throw FileSystemGenericException("We failed to load sprite
| from file: " + file.getPath(), e); } }
|
| I prefer software blowing up in predictable ways as opposed
| to doing so unexpectedly. Even Java is vaguely close to being
| what i'm looking for, however unchecked exceptions simply
| isn't acceptable from where i stand.
| hypertele-Xii wrote:
| If I had to write that kind of boilerplate every time I had
| an artistic inspiration, I'd never ship anything!
|
| We are on far apart sides of a wide industry. I couldn't
| work productively in your dream language but hey, I'm happy
| we can have our different tools for our different needs.
| More power to us! :)
|
| > let the IDE suggest to you all of the possible exceptions
|
| So, programming without an IDE becomes untenable. I use a
| text editor. It feels like you're shifting language
| features into the IDE. What's the difference between the
| compiler doing it automatically vs the IDE doing it
| automatically?
| KronisLV wrote:
| I definitely agree that we're on the complete opposite
| ends of a wide spectrum of concerns and goals!
|
| > So, programming without an IDE becomes untenable. I use
| a text editor. It feels like you're shifting language
| features into the IDE. What's the difference between the
| compiler doing it automatically vs the IDE doing it
| automatically?
|
| I very much agree with this observation, but from the
| opposite side - for many development stacks and
| frameworks, working without an IDE feels like being a
| fish out of the water, since there are numerous plugins,
| snippets and integrations that provide intelligent
| suggestions, auto-completions and warnings about things
| that are legal within the language but are viewed as an
| anti-pattern.
|
| I'd say that the difference between the two is pretty
| simple, just a matter of abstraction layers. Something
| along the lines of: - the business people
| have certain abstract goals, which they can hopefully
| synthesize into change requests - the developer has
| to implement these features, by thinking about everything
| from the high level design, to the actual code -
| the IDE takes some of the load off from the developer's
| shoulders, by letting them think about the problem and
| offering them suggestions, hints and assistance of other
| sorts to help in translating the requirements into code;
| of course, it's also useful in refactoring and
| maintenance as well, letting them navigate the codebase
| freely - the language server, linter, code analysis
| tools, plugins, AI autocomplete and anything else that
| the developer should want hopefully integrate within the
| IDE and allow using them seamlessly, to make the whole
| experience more streamlined - the compiler mostly
| exists as a tool to get to executable artifacts, while at
| the same time serving as the last line of defense against
| nonsensical code or illegal constructs
|
| In essence, the IDE gives you choices and help, whereas
| the compiler works at a lower level and makes sure that
| any code (regardless of whether written by the developer
| with an IDE, one with a text editor or an AI plugin) is
| valid. In practice, however, the parts that the IDE
| handles are always more pleasant because of the plethora
| of ways to interact with it, whereas the output of a
| compiler oftentimes must be enhanced with additional
| functionality to make it more useful (for example,
| clicking on output to navigate to the offending code).
|
| In my eyes, the interesting bits are where static code
| analysis tools and linters fit into all of this, because
| i think that those should be similarly closely integrated
| within the ecosystem of a language, instead of being
| seeked out separately, much like how RuboCop integrates
| with both Rails and JetBrains RubyMine. Our views may
| differ here once again, but i think that some sort of a
| convergence of tooling and its concerns is inevitable and
| as someone who uses many of the JetBrains tools (really
| pleasant commercial IDEs), i welcome it with open arms.
| dthul wrote:
| I don't quite follow. You always have to somehow handle the
| case the file does not load successfully. In exception
| languages that handling might be implicit (raise an exception
| and crash your program) and in "errors as values" languages
| you at least have to acknowledge that it could go wrong with
| something like `image.unwrap()` (which turns it into a
| program aborting panic).
| wvenable wrote:
| One of my personal favorite examples of exception handling
| was a small GUI app with a single top-level exception
| handler at the event loop that displayed an error message
| and continued.
|
| That application was extremely robust. You try and save a
| file and 100 different things could go wrong (network drive
| unavailable, file is read-only, etc) but it nicely
| recovered and you could see what the problem was, correct
| it, and re-save. One single exception handler for the whole
| app.
| hypertele-Xii wrote:
| > You always have to somehow handle the case the file does
| not load successfully. In exception languages that handling
| might be implicit
|
| I.e. you _don 't_ have to handle it.
|
| Until you're polishing the program for a stable release,
| that is.
| dthul wrote:
| Right, in both approaches you can choose to handle the
| error by ignoring it and crashing. In "errors as values"
| languages you have to make that choice explicit by
| marking the line with `unwrap`. Saying that this
| requirement is "the single most unproductive mis-feature
| a language could have" is extreme hyperbole, no? Adding
| `unwrap`s during development to imitate implicit
| exceptions for fast prototyping takes no time or thought
| at all.
|
| On the contrary, when you later want to polish your
| program for release these explicit markings make it very
| easy to find the points in your code where errors can
| occur and which you don't properly handle yet.
| hypertele-Xii wrote:
| Okay, if there's a simple way to mark some code as
| "compile this even if it's wrong", it's only a minor
| annoyance.
|
| But the commenter I responded to seemed to me to be
| wishing for a language that explicitly disallows that.
| Maybe I misunderstood?
| cletus wrote:
| I'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....
|
| [2]: https://abseil.io/docs/cpp/guides/status
| foxhill wrote:
| > To make it "readable" a bunch of functions had to be created.
|
| and in doing so created code that was far more self-documenting
| and evidently correct. the counter-example was no where as easy
| to reason about (imo), even for the simple example.
|
| > You can't step through that code with a debugger.
|
| i don't think it's fair to comment the content of the idea
| based on the quality of existing debuggers. in any case, i
| don't think it's true that you can't step through this code on
| a debugger (generally): VSCode & Roslyn have no issue with this
| sort of structure in C#.
|
| > The error messages may be incomprehensible.
|
| the ones in the example may be. i've worked in a large code-
| base using this approach before, and there was rich error
| information. transformations of failure states (i.e. the Error
| values) are easier to do with context, as opposed to your
| catch-block which has no knowledge of the context in which an
| exception was thrown.
|
| > I much prefer Rust's or Go's version of this, which is
| instead imperative.
|
| go's (err, val) "error handling" paradigm is, imo, it's worst
| feature. i can't talk for rust. whilst your assertion that a
| failure condition is impossible to reach may be true for the
| code you write today, it almost certainly wont be in the
| future..
|
| how many error dialogues have you seen saying some variant of
| "unreachable state reached"? :)
| kevindong wrote:
| Checked exceptions are horrible to work with, but they require
| the programmer to do something about them at the level
| immediately prior while as unchecked exceptions could just
| bubble up to an unexpected point in the stack which is arguably
| worse.
| zvrba wrote:
| > 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 wrote:
| It'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-...) as an alternative.
| josefx wrote:
| A null doesn't contain any information about what went
| wrong and unless you religiously check your objects for
| null values at every turn you just turned a clear stack
| trace into a search for waldo at the international waldo
| impersonators meetup.
| dthul wrote:
| Note that in Kotlin the return type is `Int?`, not `Int`.
| You can't forget to check for null because the compiler
| enforces it.
|
| To your first point: Another example in the design space
| would be Rust which works very similarly to Kotlin but
| returns more information in the failure case.
| zvrba wrote:
| > Note that in Kotlin the return type is `Int?`, not
| `Int`.
|
| How does that fare with unnecessary boxing of primitives?
| kreetx wrote:
| If the function isn't total (as in: for every string
| there is an int) then the boxing is necessary, no?
| Cthulhu_ wrote:
| But the mechanism is just wrong; Exceptions are heavyweight
| and should only trigger with unexpected issues, bugs that a
| developer wants to see a stacktrace for.
|
| I mean in this case you could consider it developer error; a
| developer tried to parse an integer without first validating
| the input and checking if it COULD be parsed. But it's
| normalized to just "let it crash", instead of writing
| additional pre-check code.
|
| With errors as values - like Go has normalized - instead of a
| big, expensive, potentially panicky exception, you just get a
| lightweight error option back. With the more functional
| approach of Either, you are basically forced to deal with the
| "but what if I can't parse it" code branch.
| cletus wrote:
| So here's a pattern neophytes often end up implementing:
| if (isValidInput()) { parseInput(); }
|
| It seems like a good idea but it's not, for several
| reasons:
|
| 1. It requires an explicit second step. Worse, the
| behaviour of the second step may be undefined if the first
| step didn't take place. Either way it's just error-prone;
|
| 2. While this may be correct for a static string with
| static rules, what if this isn't stateless? You've now
| introduced a race condition;
|
| 3. If each step requires context (eg options) you need to
| correctly pass them to both. This is another potential
| source of error; and
|
| 4. You've created what's likely an artificial
| differentiation between valid and invalid input. What
| happens when that changes and you need to distinguish
| between invalid (not a number) and invalid (number out of
| range)?
|
| (2) is particularly common in security contexts. You'll see
| this pattern as: if (userCanEditPhoto())
| { editPhoto(); }
|
| Usually you can't do this, as in the primitives won't be
| there. This is for good reason. Engineers should instead
| change their mindset to implementing these things as an
| atomic action that gives reason for failure. Exceptions are
| one version of this. They're just bad for other reasons.
| scbrg wrote:
| > a developer tried to parse an integer without first
| validating the input and checking if it COULD be parsed
|
| Is there a meaningful difference between validating that
| input _can_ be parsed and parsing it?
|
| Any validator that doesn't actually _parse_ the data -
| according to the same rules the parser uses - runs a risk
| of incorrectly passing /failing certain cases, no?
| wvenable wrote:
| Java needs a TryParseInt (sorta like C# has) so can you use
| either one as appropriate.
|
| There are actually two main use cases for integer parsing:
| one where the value is expected to be an integer (you're
| parsing a file format) and the other where it's just likely
| not to be an integer (getting input from the user).
| jeffreygoesto wrote:
| To me this is not "ecxeptional" at all, as it is easy to call
| that function with a non number and it should return
| "normally" that the input was not an integer. I pretty much
| prefer the rust Result or C++'s expected<>.
| zvrba wrote:
| It has nothing to do with "exceptionality". It has to do
| with the method not being able to fulfill its contract.
| Rust's returning `Result<>` is a _different contract_.
| rini17 wrote:
| To 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 wrote:
| On Windows, there is an API for this -
| https://docs.microsoft.com/en-us/windows/win32/api/errhandli...
| - you can save the callstack there, before a C++ exceptions
| happens.
| oftenwrong wrote:
| I 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
|
| 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
| layer8 wrote:
| In 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 wrote:
| The 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 wrote:
| > 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]
| zvrba wrote:
| > 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.
| jollybean wrote:
| "> 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."
|
| I don't see how this logic flows.
|
| Exceptions do give you the alternative to recover, sure, but
| the author is saying 'the kinds of errors that produce states
| where you should stop the program ... use exceptions'.
|
| You're not really disagreeing it seems.
|
| Where you might disagree, is that the author is indicating the
| 'recovery cases' are more suited to being straight error
| returns while you're hinting at exception recovery.
| zvrba wrote:
| > You're not really disagreeing it seems.
|
| The author wrote "stop the program". I took it to mean
| literally: stop the program, i.e., exit immediately, i.e.,
| crash. That's not acceptable in long-running "service"
| programs.
| wvenable wrote:
| There's no need for even that complexity. If base exception
| type had a single property, something like "CanRetry", all
| exception handling would be simple.
|
| Because when it comes to exceptions there are really only 2
| things you can do: abort the current operation or retry it.
|
| The code generating the exception will know which of these is
| appropriate and the try/catch handler is what would restart or
| cancel the operation.
|
| The exact details of the exception are for debugging not for
| program flow.
| Joker_vD wrote:
| May I interest you in looking at Java? It has some interesting
| lessons wrt your proposition.
| p2t2p wrote:
| > 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?
| zvrba wrote:
| Yes. What I'm thinking of are _not_ checked exceptions, at
| least not at compile-time.
| p2t2p wrote:
| Can you elaborate then?
| zvrba wrote:
| I did in the original post, and that's why I used C++ as
| example instead of Java. A method declares `throws X, Y,
| Z` and _runtime_ checks that no other exception escapes.
| No source changes needed if you add W to the list. And if
| some other exception escapes, it's wrapped in
| `UnexpectedException` that is reserved for and throwable
| only by the runtime.
| golergka wrote:
| There'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.
| XVincentX wrote:
| On the same topic: When An Error Is Not An exception Series:
| https://dev.to/vncz/series/6223
___________________________________________________________________
(page generated 2021-08-30 23:02 UTC)