4 ms·
That would be the most basic thing, but you'd never be able to use it (toy code written for an internet comment aside). 1. Errors quickly lose their usefulness
by 9rx 2y ago
That would be the most basic thing, but you'd never be able to use it (toy code written for an internet comment aside).
1. Errors quickly lose their usefulness if you simply pass them up the stack. Even just on the surface, if you don't even know where the error came from, good luck making sense of it. Other languages have tried to avoid that problem by having things like "sidecar" handlers that can handle the error elsewhere, but in the end that's just moving code around. You haven't actually solved the overhead of needing to do something with the error.
2. Errors simply passed up the stack are, more often than not, going to leak implementation details. Consider a function that fetches data from a SQL database, where the underlying operations produce a "SQLNoResults" error. If you let that flow through, callers are going to start to rely on it. Now, imagine new requirements dictate that you need to fetch the data from an HTTP service instead. If you continue to simply pass the error along, now callers are going to get a "HTTPNotFound" error instead, breaking their usage. Not a good situation.
- Dylan16807 2y ago1. It's not just moving things around. Often you want a segment of code to all do the same thing on error. 2. There are places where you want to abstract your errors, but those places are not every function call or even most function calls.
- 9rx 2y ago> Often you want a segment of code to all do the same thing on error. Do you mean often you want multiple segments of code to all do the same thing on error? If there is only one segment then you well and truly have just moved things around. Multiple segments all doing the same thing on error would give more justification to centralizing functionality, but at the same time if you have multiple segments of code all doing the same thing you've probably not thought your design through. Papering over design mistakes with language features is commonly done, but I'm not sure it is something to strive for. > but those places are not every function call or even most function calls. If the original error is your own you don't need to abstract it, but if you are passing your own errors through multiple levels of indirection you've, again, probably not thought your design through very well. Papering over design mistakes with language features is commonly done, but I'm not sure it is something to strive for.
- Dylan16807 2y ago> Do you mean often you want multiple segments of code to all do the same thing on error? If there is only one segment then you well and truly have just moved things around. If a segment has 6 function calls and you want the same error handling for each one, you can't get rid of the boilerplate with the current language. > If the original error is your own Assume the original error is not my own then.
- 9rx 2y ago> If a segment has 6 function calls and you want the same error handling for each one, you can't get rid of the boilerplate with the current language. My deepest sympathies for the person who has to respond to the resultant error once you've collected your paycheque and have moved on to the next project. Which of the six functions produced the error? Nobody knows. That may be all well and good for a contrived internet comment example, but if you write real code like that someone's life is soon going to become a living hell. Every other language has recognized that you can't have the same error handling for each of the function calls. Even where they have special error handling semantics have special ways to ensure that the handling is different in each case. Why do you think it would work in Go? > Assume the original error is not my own then. Then you've forever hitched your horse to their code. A better or more performant replacement comes along in the future and you want to use it instead? Too bad. You can't without breaking your own API – and for what reason? But, okay, we accept that you like to live life on the edge (or come from the Javascript world and thus don't know any better) and if the people using your functions start having breakage, too bad so sad. However, if you're just passing values through from another package, why are you really offering your callers in the first place? Why don't they just use the other package directly?
- Dylan16807 2y agoI like how the first half of your reply assumes the difference between errors disappears, and the second half of your reply assumes the difference doesn't disappear. Just because I share handling doesn't mean I would throw out all the information, and if I have a filesystem error while setting up a file I'm going to do the same cancellation paths no matter what particular filesystem error it is. > Then you've forever hitched your horse to their code. A better or more performant replacement comes along in the future and you want to use it instead? Too bad. You can't without breaking your own API – and for what reason? No, I did not say that. What I said is that the place to prevent that is not every function call. If my code goes 4 functions deep, I need at least one of them to handle errors I didn't cause, or convert them into my own errors for the sake of a stable API. But many of the other functions can pass errors through.
- valenterry 2y ago> Errors quickly lose their usefulness if you simply pass them up the stack. Nope, not in languages where you have a stacktrace attached. > Errors simply passed up the stack are, more often than not, going to leak implementation details. That's why in a good language you can just wrap them at the right level of abstraction.
- deleted 2y ago[deleted]
- 9rx 2y ago> Nope, not in languages where you have a stacktrace attached. Like you say, the stack trace needs to be attached. For that you, at very least, need a "sidecar" handler if not done so in the same execution path, as we discussed earlier. Did you, uh, forget to read the thread? > That's why in a good language you can just wrap them at the right level of abstraction. You can move the logic around, but you can't avoid it, as we discussed earlier. Did you, uh, forget to read the thread?
- valenterry 2y ago> Like you say, the stack trace needs to be attached. For that you, at very least, need a "sidecar" handler if not done so in the same execution path, as we discussed earlier. Did you, uh, forget to read the thread? I just read it again, but I'm not sure what you mean. And sorry, I'm not familiar with that terminology (sidecar) but from the perspective of the user/developer, does it matter? As system or library developer, I don't need to do anything - I'll have the stacktrace available when I need it. There is extra code necessary. (one has to be mindful of the performance, but that's it) > You can move the logic around, but you can't avoid it, as we discussed earlier. Did you, uh, forget to read the thread? Doesn't have to do anything with moving logic around. Let's say you are function foo and you call other functions and one of them is bar and it will fail with barError. Then, to avoid breaking your (= foo's) consumers if bar changes its internals, you simple wrap bar's error with your own. That can be as simple as doing `bar.mapError(barError -> fooError(cause = barError))` and that's it. That is all I wanted to say. Then, depending on the language, you stil don't have to repeatedly do "`if err != nil {}`" or so. There are enough alternatives, e.g. monadic error handling like in Haskell or macros like in Rust.