7 ms·
An epic treatise on error models for systems programming languages
- Galanwe 2y ago> Almost all catastrophic failures (92%) are the result of incorrect handling of non-fatal errors explicitly signaled in software So in a laps of 15 years (2010-1025), they hand picked 20 bugs from 5 open source filesystem projects (198 total), and extrapolated this result. That is not science.
- dbtc 2y agoIt is writing, though. You've picked one sentence out of rather more and made a negative statement about it. What is that?
- perching_aix 2y agoCritique. The idea is that the evaluation in the article may not have been representative, but this quote is self-contained enough to be representative of its own stance. Instead of going for rhetorical KOs, how about remaining constructive? You're well aware that this is what they meant, you could have just written it plain out if you disagree.
- typesanitizer 2y agoHi, blog post author here (unrelated to paper authors). > So in a laps of 15 years (2010-1025), The paper was published in 2014, so the period is 2010-2014, not 2010-2025. > they hand picked 20 bugs from 5 open source filesystem projects (198 total) The bugs were randomly chosen; "hand picked" would imply that the authors investigated the contents of the bug reports deeply before deciding whether to include them (which would certainly fall under "bad science"). The paper states the following: > We studied 198 randomly sampled, real world fail- ures reported on five popular distributed data-analytic and storage systems, including HDFS, a distributed file system [27]; Hadoop MapReduce, a distributed data- analytic framework [28]; HBase and Cassandra, two NoSQL distributed databases [2, 3]; and Redis, an in- memory key-value store supporting master/slave replica- tion [54] So only 1 out of 5 projects is a file system. > and extrapolated this result. That is not science. The authors also provide a measure of statistical confidence in the 'Limitations section'. > (3) Size of our sample set. Modern statistics suggests that a random sample set of size 30 or more is large enough to represent the entire population [57]. More rigorously, under standard assumptions, the Central Limit Theorem predicts a 6.9% margin of error at the 95% confidence level for our 198 random samples. Obviously, one can study more samples to further reduce the margin of error Do you believe that this is insufficient or that the reasoning in this section is wrong?
- Galanwe 2y ago> The paper was published in 2014, so the period is 2010-2014, not 2010-2025. Oh indeed, my bad. > The bugs were randomly chosen; "hand picked" would imply that the authors investigated the contents of the bug reports deeply before deciding whether to include them No, no, they were not randomly chosen, they even have a whole paragraph explaining how they randomly picked _from a pool of manually selected bugs_. Their criterion of selection varied from "being serious", to "having a lot of comments" and "they can understand the patch", or "the patch is not from the reporter". > The authors also provide a measure of statistical confidence in the 'Limitations section'. This is a measure of confidence of their random sampling being representative of the hand picked bugs... > under standard assumptions, the Central Limit Theorem predicts a 6.9% margin of error at the 95% confidence level I would love to see them prove the normality assumption of bug root cause distribution. Also, the whole categorization they do seem purely qualitative. This paper seems to lack rigor to me.
- gtirloni 2y ago> The whole paper is a statistical joke. I have been guilty of such kind of comments and I have come to realize they do nothing to further my point. I would suggest to read the guidelines because they are very well written about this: https://news.ycombinator.com/newsguidelines.html https://news.ycombinator.com/newsguidelines.html
- Galanwe 2y agoFair point, I edited my answer to be less dismissive of the paper. Thanks for the considerate feedback.
- epr 2y ago> We studied 198 randomly sampled, real world fail- ures reported on five popular distributed data-analytic and storage systems, including HDFS, a distributed file system [27]; Hadoop MapReduce, a distributed data- analytic framework [28]; HBase and Cassandra, two NoSQL distributed databases [2, 3]; and Redis, an in- memory key-value store supporting master/slave replica- tion [54] So, analyzing databases, we picked Java, Java, Java, Java, and one in C. This does not seem very random. I suppose this may provide insight into failure modes in Java codebases in particular, but I'm not sure I'd be in a hurry to generalize.
- tuukkah 2y agoFrom the end: > Pithy statements such as “only use exceptions for exceptional cases” or “Let it crash”, while catchy, do not do justice to the complexities that programmers need to deal with when deciding how to define, propagate, handle and reason about errors. I think the author is on to something here w.r.t. how current programming languages and conventions brush over what good code might need.
- lo_zamoyski 2y agoWhich languages and in what ways? And compared to which "non-current" languages and in what ways?
- nahuel0x 2y agoMissing piece: Common Lisp continuable condition system.
- nesarkvechnep 2y agoMore like restartable. You may be conflating it with continuations.
- typesanitizer 2y agoCopying my comment from the Lobste.rs thread (https://lobste.rs/s/az2qlz/epic_treatise_on_error_models_for_systems https://lobste.rs/s/az2qlz/epic_treatise_on_error_models_for...) > Hi, author here, the title also does say “for systems programming languages” :) > For continuations to work in a systems programming language, you can probably only allow one-shot delimited continuations. It’s unclear to me as to how one-shot continuations can be integrated into a systems language where you want to ensure careful control over lifetimes. Perhaps you (or someone else here) knows of some research integrating ownership/borrowing with continuations/algebraic effects that I’m unfamiliar with? > The closest exception to this that I know of is Haskell, which has support for both linear types and a primitive for continuations. However, I haven’t seen anyone integrate the two, and I’ve definitely seen some soundness-related issues in various effect systems libraries in Haskell (which doesn’t inspire confidence), but it’s also possible I missed some developments there as I haven’t written much Haskell in a while.
- ForTheKidz 2y agoYes, I can see how if you view lifetime management as core to systems processing lisps might be unappealing to you.
- fanf2 2y agoYou don’t need continuations to implement resumable errors. The trick is simply to not unwind the stack when an error happens. http://axisofeval.blogspot.com/2011/04/whats-condition-system-and-why-do-you.html http://axisofeval.blogspot.com/2011/04/whats-condition-syste...
- 2y ago
- namaria 2y agoYou know, there is a programming language based on a PhD thesis on fault tolerance. This programming language is built around a VM that implements lots of great design on error handling. And there is a functional programming language that compiles to the bytecode that this VM runs. It is a delight to use, gives you all the fault tolerance goodness for free, but the name is often cursed in HN threads for some reason. edit: I might be giving some random episode too much weight. Seems like Elixir and Erlang are actually quite well liked around here
- ejiblabahaba 2y agoWhich language is this? I'm sure some people can clue this together from the hints, but I'm not one of them.
- jasinjames 2y agoProbably Java/Clojure
- namaria 2y agoI meant Erlang/Elixir Java is also based on a doctorate research on fault tolerance?
- nesarkvechnep 2y agoNo, God, no. Java isn’t based on academic research.
- flir 2y agoErlang/Elixir is also a possibility. But I don't think either Java or Erlang were based on a PhD project? (Edit: I just looked it up, and Joe Armstrong's PhD was 2003)
- kbn 2y agoI think erlang
- 2y ago
- chrishill89 2y agoWhat a great resource. I’ve been in the errors-as-values (like Rust and Haskell) camp for over a decade. But I think you need to go a bit beyond that philosophy (i.e. not just as a slogan) in order to truly unlock what treating errors as values can be like. Errors as values are just values. Not any more complex than other values. But except in cases like `None | Error` (no regular value or an error) you at least double the work that you need to do. The thing about the “happy path” is that it’s just one channel of information. With errors you layer on one more channel. I think that has lead me to be careful about treating errors in the same way I treat other kinds of values. Because simplifying errors down to just one value, like a string, simplifies the whole error channel. But how is error-as-string useful if you are using the code as a library and not just erroring out and letting the end-user/operator read it with their primate brain? Well that lead me to add variants, sum types for all the different error possibilities. Along with associated data like how this int was larger than some exected bound. Already here I run into a sort of sub-error problem: only advertising what variants of an error that I can return from a function. It seems that this isn’t supported directly in Rust, which is what I was using at that time. So if I just advertise that I can return all the error variants (which is a soft lie) then I might have to deal with them downstream. More expressive error values seem to both (1) burden the implementation (sub-errors) and (2) the clients who have to consume them. So do I use less expressive errors so that they become easier to handle for client code? Well it seems here that I’m still stuck treating errors as special values. Because I still seem hesitant to treat errors in their full generality as values. Think of a value: 1. It’s not just a single “atom”/scalar like an integer 2. It can be in a list or a dictionary/map or on a stack or some other data structure 3. It can be transformed with a map or reduce operation since you either want to transform the error or leave out some details Maybe the library should provide a list of errors. But the client only cares about the last one. So it takes the head of the list. Maybe the library should provide a detailed error containing associated data about the bounds, expected and actual. But the client just cares about the error type and elides the rest. Maybe this doesn’t seem useful for errors. But take the `Hurdle` type in the article. These are effectively a mandatory (product type) list of warnings. Maybe the client code only cares about five out of eight of them so he filters out the rest. Or he doesn’t care about them at all so he just ignores them. And this seems to lead into iterators and lazy evaluation. In high level languages at least we’ve come to a point where we value pretty functional, declarative, and coarse transformation of data. “Coarse” in the sense that we use a few datastructures and operations to process the data without worrying about special datastructures and micromanaging control flow. But crucially these constructs don’t have to allocate N intermediary collections. They can be lazy. So maybe errors (and warnings (hurdles)) should be as well. Maybe you can have a very rich error/warning declaration while only generating them when the client code wants it. And what happens when you make a nod (at least) that all the error computations can be elided if only, say, you just kill the application on the first sight of any problems (like in a script)? People are incentivized to treat errors as any other value without fearing that it is wasted effort (computation). Now on the other hand I do program in Java. And personally, philosophically, I am fine with exceptions for the type of application advocated in the article: report errors way up the stack. But there I’ve been running into the same problem: treating exceptions as not-quite-values. For any other custom class I would create however many fields and methods I need. But for custom exceptions I just give them a name. And maybe pass a string to the constructor. Which I could do for a built-in or third-party error. (So effectively I just give them a new name.) But why? There must have been a block in my mind. Because Java exceptions are just classes. And they can have fields. And ultimately we log errors that bubble all the way up. So why not record all the associated data as fields on the exception and use the error handler (with `instanceof`) to gather up all the data into a structured log? That should be a ton better than just creating format strings everywhere and sticking the associated data into them. And of course you can do whatever else with the exception objects in the error handler; once it’s structured you can use the structure directly, without any parsing or indirect access.
- demurgos 2y agoVery nice article. I agree with the theses and the resulting design makes sense. Regarding context handling, one issue that I struggle with is where the context should be attached: by the caller or callee? In particular, in the context of intermediate libraries, I would like to limit needless work attaching the context if the caller already handles it (some sort of opt-out). Usually I just attach the context in the callee as it's the simplest, but I'd like some systematic/reusable approach that would control this. It would be nice if it fits in the type system too. I use a bunch of languages like Rust, TypeScript, Kotlin, Python and it feels like a larger design issue. To give a more concrete example. In Node, `fs.readSync(path)` includes the error path when it fails: the callee attaches the context to the error. In Rust, `std::fs::read(path)` does not attach the path to the error: the caller is responsible for the context. I would like some lightweight way to control if the file read should include or not the path. The ancestor scanning example from the article is a case where caller context is good IMO, but usually callee context is better for unrecoverable errors. Since it's contextual, having control would be nice.
- jandrewrogers 2y agoI've spent a lot of time thinking about error context management in the context of C++. There seems to be a pretty deep conflict between having consistent elegant error context and efficient high-performance code. Explicit delegation works but only sometimes without incurring a runtime overhead. Even trying to find a reasonable and robust middle ground has proven to be elusive. Expanding on your caller/callee example, sometimes both are far removed from the path, e.g. only having a file descriptor. There are plenty of ways to resolve that file descriptor into a path but one that consistently has zero overhead except in the case where it is required to contextualize an error is not trivial. Being able to resolve outside context implies the management and transmission of context in cases where it is not needed. I am also coming around to the idea that the error handling context needs to be at least partly co-designed with the logging APIs and internals to have a little more flexibility around when and where things happen. In most software neither of these is really aware of the other.
- jstimpfle 2y ago
- sirwhinesalot 2y agoReally nice article that covers quite well the complexities involved in dealing with errors. "They're just another kind of value" and "Just crash and restart" are both wrong, error handling is much more complex than that. Different kinds of errors need to be handled in different ways in various contexts as the article points out. I won't comment on the error model proposed in the article as I find that without actually trying out stuff like this it's very hard to get a "feel" for it, but at first glance it looks a bit over-engineered. I think the gist of what makes a good error handling system is as follows: Certain errors are contract violations/bugs that should not occur in normal program operation, and should not "taint" the code with explicit error handling. The language should provide a mechanism that allows to abort the current "task" when such an error occurs and trigger some error handler. Exceptions are good for this, but it falls on the programmer to define a "task boundary" appropriately. Defensive try/catch all over the place is not that, and is usually done because... Certain errors are part of normal program operation and the programmer should be reminded to deal with them. Exceptions are terrible for this but Result<T,E> types are great. Having to use Result<T,E> in situations where the error is actually not normal part of program operation (e.g., you're accessing a key on a map that can only be missing due to a bug) is annoying but is easily solved by providing two separate APIs (like Python's __getitem__() and get() methods on dict). Often times an expected error has no good way to be dealt with at the point it occurs, so there should be an easy way to just abort the current "task" when that happens. This is your ".expect()" or just a giant pile of ? propagation in Rust but Rust panics don't carry enough information by default like you can with an Exception type. Note that is different from the two API version above. You use the "panicking on error" API only in cases where the error is not expected, whereas you use some .expect() equivalent as a way to abort the current task for an expected error. Sometimes you also want to collect multiple errors and abort only after every error is collected, like in a typechecker. This is often done with "NullObject" patterns or "ZII" (most of the time handled very poorly) but there are ways to do it right where the error case "poisons" the data up to the task boundary but cannot propagate further than that (turning into a Result<T,E> equivalent at that point). The most important part is understanding task boundaries. I find most programs (including my own) do a very poor job of this. The information contained in the error (sometimes called the context) is very tricky to deal with as what constitutes relevant information for a library or for an app using that library can be very different and there's a performance cost involved with its collection.
- gavinhoward 2y agoI searched for conditions and restarts. I did not find them. That makes me sad because that is what my systems programming language uses. For context, conditions and restarts come from the Lisp world. They do not unwind the stack. Instead, they run an error handler on error, and if it handles the error, you have the option of restarting the operation that failed. I have implemented them in C; they are great for systems programming.
- zozbot234 2y agoThat's just an overly fancy version of ON ERROR RESUME NEXT.
- gavinhoward 2y agoNo, it is much more powerful. ON ERROR RESUME NEXT is equivalent to a catch all in a try catch. Conditions and restarts let calling code set up error handlers, and if one of them handles the error, it would be like starting over from the top of the try block.
- sesm 2y agoDid you publish your implementation anywhere? Would be very interesting to see. Thanks!
- gavinhoward 2y agoI did, but my Gitea instance stopped working. I am trying to replace it.
- layer8 2y agoYou can, of course, do the equivalent with explicit callbacks and a bit of additional boilerplate. However, I have never felt the need to do that (as opposed to, say, having a numRetries parameter), so I always wonder how people who advocate for such a mechanism design and modularize their code. It feels like it leads to dependencies on implementation details, as opposed to only relying on a procedure’s interface.
- CyberDildonics 2y agoIs this an "epic treatise" ?
- diffxx 2y agoIt sometimes feels like there is a strange resistance to defining ad hoc sum types. If your language has good pattern matching capabilities and an expressive syntax for declaring data types, then this scales very well. For example in a pseudolanguage: data FooResult = Number | Error def foo(n: Number): FooResult = ... foo(42) match { case n: Number => print(n + 1) case e: Error => // handle error case } This gets you very far most of the time. If the tag overhead matters (which should largely only matter in a hot loop), then try to refactor so that the hot loop doesn't error out. Granted in many languages the above cannot be expressed succinctly and is often avoided for this reason.
- uzerfcwn 2y agoThat looks more like ad hoc union types than sum types. What happens if you do: data FooResult = Number | Number If you can't distinguish the two cases, then it means they're not an actual sum type, since e.g. the set size is |FooResult| = |Number| whereas a sum type should have 2*|Number|. The reason ad hoc union types are avoided in Hindley-Milner (HM) languages is that their usability depends on subtyping, which is incompatible with HM. You could get around this by saying that ad hoc sum types require the types to be distinct, but this would greatly impede usability. For example: ErrorA = FileNotFoundError | ParsingError ErrorB = FileNotFoundError | PermissionError ErrorC = ErrorA | ErrorB // Oops, trying to ad hoc sum FileNotFoundError twice The tried and true solution in HM is to use generic types like Result<A,B>, where you separate the cases with labels Ok and Err. On the other hand, languages with subtyping aren't that averse to ad hoc union types. TS and Python already have them and C# is adding them soonish [1]. [1] https://github.com/dotnet/csharplang/blob/main/proposals/TypeUnions.md#ad-hoc---ad-hoc-unions https://github.com/dotnet/csharplang/blob/main/proposals/Typ...
- deleted 2y ago[deleted]