5 ms·
"The lack of exceptions makes writing correct code really tedious." Tedious up front often translates into time saved down the road. Exceptions are often an e
by jb613 11y ago
"The lack of exceptions makes writing correct code really tedious."
Tedious up front often translates into time saved down the road. Exceptions are often an easy way out while accruing future debt.
Unfortunately, our industry cannot easily measure the time saved down the road and so cannot properly value it. However, many with long term interests in a project will often spend on the "tedious" costs up front to avoid potential future costs.
- pacala 11y ago> Exceptions are often an easy way out while accruing future debt. Is there perhaps a concrete illustration on how using exceptions causes technical debt, especially in a GCed language like Go?
- spyspy 11y agoThe advantage with go is that errors have to be handled right away by the thing that may have caused it. The is versus, say a try/except with a dozen lines of code in the try, and then something like `except KeyError: pass`. You may have no idea which line caused the error or why it did so.
- mononcqc 11y agoThe general principles for fault tolerance require Separation of Concerns vis. Error Encapsulation (make sure that the contagion doesn't spread), Fault Detection (make sure that you know that someone is infected), and Fault Identification (you have ebola!). Error encapsulation (and this applies equally to modules, components, systems, architectures, organizations) is invariably best done at the lowest level possible, which invariably breaks #3 and #4 (fault detection and identification). what if you have different classes of exception handling mechanisms available for each of these cases? What about languages that may support exceptions, option types, tagged values, multiple return values, signals, continuations and/or whatever mix of them that exists? The problem with handling everything right away is that it lacks flexibility to handle things in what could be the optimal manner. "One true form of exception handling", to me, sounds as reductionist of an approach as "one true form of concurrency", "one true programming language paradigm", or whatever. Treating all error conditions / exceptions with the same mechanism will generally ensure that you pick similar stances on encapsulation vs. detection and identification for all error conditions / exceptions, unless you decide to be extra careful about all of that. Using multiple mechanisms will allow you to pick, case by case, which one you feel is worth breaking depending on the nature of the fault and what your specific application or system requires. I feel Go is doing a pretty bad job at this.
- jb613 11y ago"The problem with handling everything right away is that it lacks flexibility to handle things in what could be the optimal manner." You can pass the error (or another) up the stack. The language is flexible.
- mononcqc 11y ago"Passing up the stack" is fairly minimalistic in an environment where you could be running thousands of goroutines, some (many?) of which may share memory. If you do it that way, you seem to forego a lot of error encapsulation straight away and quickly find yourself into undefined territory.
- jb613 11y agodoesn't matter whether no goroutines or thousands - you either handle errors or pass upwards. Anyhoo, just pointing out that it has the flexibility to do either.
- parenthephobia 11y agomononcqc is pointing out that those aren't the only options: other error-handling mechanisms exist. The options are not either "handle the error right now" or "make the caller handle the error". In fact, in go those aren't even the only options, since you can pass an error object up the stack using panic/recover. An alternative model could have error handlers registered somewhere and be invoked at the point the error occurs, either choosing to pass control to another part of the program, or resolve the issue and continue processing. That could, of course, be implemented in vanilla Go, but then everything has to agree to do that. Some systems are able to distinguish the point where you recover from an error from the point where you report an error: e.g. a batch processing system may recover from an error by skipping an item to be processed (and perhaps storing it in a list of failed items to be inspected later) and another "catch" handler further up may decide whether and how to display a diagnostic about the issue. Of course, this logic could be embodied as a "reporting" object which is passed down to the batch processing algorithm, but hooking into the exception handling logic means that you can use the existing error-reporting infrastructure. Common Lisp has a system of "restarts" where one can configure multiple ways for a handler to respond to errors other than simply passing it up or carrying on. For example, a batch processing system might set up restarts to allow items to be skipped, retried now, or retried after the batch finishes. The batch processor itself might have restarts for retrying the batch or rescheduling it on another node. Above the batch processing algorithm, an exception handler can look at the situation from a high level and decide what to do: e.g. it may determine that the batch has grown too big for the node and reschedule it on another one. Until the decision is made, the stack is not unwound, so if I decide to restart an item from the batch, I just jump right back into the still-running batch processing function. This design allows the mechanism for how to actually deal with an error condition to be separated from the decision as to which mechanism to use. It's very much like breaking into a debugger, except the program can debug itself. In Erlang, a program handles errors by keeling over, dead. Because an Erlang system is (meant to be) designed as a swarm of cooperating processes, an individual process can just die when something goes badly wrong and its compadres are expected to have registered an interest in knowing that this has happened. A common design pattern is to have a dedicated monitoring handling process which knows how to orchestrate things so that one broken process doesn't cause a cascading failure through the whole system. In the running example, a batch processing system might spawn a process for each batch, and a monitor process might check for batches which die and then decide whether and where to restart them. That said, Erlang still has a fairly traditional try/catch system for when that makes more sense. And errors are objects, so you can return them up the call stack as well. There are many more mechanisms for error handling which aren't simply "handle here or pass up".
- deleted 11y ago[deleted]
- parenthephobia 11y agoConversely, the disadvantage with go is that errors have to be handled right away by the thing that may have caused it. Sure, with a `try`, you may have no idea which line caused the error or why it did so, but that isn't what matters. After all, if a function returns an error, you don't know what line of code actually caused the error. (Especially in go, since errors lack backtraces.) With "exceptions" you know that something within the block failed and passed the information about what went wrong to the catch/except/rescue block. Problems arise if the block fails to tie up its own lose ends, but that's a problem that exists without structured exceptions: if a function might return an error you still have to take it on faith that it arranged for clean-up of any state that, outside the function, would be problematic. Fundamentally, try { foo() bar() } catch e Fred { cleanup1() } catch e Barney { cleanup2() } isn't really any different from if e := (func()error{ if e := foo(); e != nil { return e } if e := bar(); e != nil { return e } return nil })(); e != nil { if _, ok := e.(Fred); ok { cleanup1() } else if _, ok := e.(Barney); ok { cleanup2() } else { return e // assuming this function returns error } } except that one of them is far more readable. It's potentially different, of course, inside foo(), where it might unexpectedly call a function which raises. But because recover exists in the language, robust functions must assume that any function call, or various built-in operations[1], might result in code further up the call stack recovering from a panic, so it must make sure it will fix its inconsistent state when the stack is unwound. Additionally, go is inconsistent in that some errors - e.g. array index out-of-bounds - cause panics instead of indicating errors normally. That's understandable, since having to type if c, ok := array[i]; ok { err = fmt.Errorf("Array index out of bounds") return } else { // do something with c } every time you wanted to do c := array[i] would be really tiresome. [1] Such as: comparing non-comparable objects via references of interface type; dereferencing a null pointer or interface; out-of-bounds array/slice/string indexing; division by zero; sending on a closed channel. All likely occurrences.
- tankenmate 11y agoGC means that most memory leaks cause by poor exception handling code are auto-magically fixed but it doesn't fix other resource leak problems; file handles, memory maps, locks, etc.
- Skunkleton 11y agoNope. Because like most popular language constructs they can be used effectively or poorly. There are a ton of completely valid arguments on both sides.
- kazinator 10y agoLack of non-local dynamic control transfers with unwinding is the real deal breaker. Exceptions are a red herring; they are just something you can build if you have dynamic control transfers with unwinding. And if you have macros, or at least some kind of access to the language to bend the syntax. Plus perhaps other goodies like testing whether some object X is of a type which is a subtype of T so you can make exception handling frames decide whether the elevator stops here. The underlying control mechanism not being there makes the language crippled. Just give me C. Then I also have forty years of research out the window: but but at least with setjmp and longjmp. How does Bash recover to the top level prompt when the scriptage fails deeply nested in some function calls? Why, longjmp! I will fake the unwinding if I have to.
- kjksf 10y agoGo has panic()/recover(), which is better than setjmp/longjmp (thanks to GC you won't leak memory like you would in C with setjmp/longjmp). I'm not saying using panic()/recover() is a good idea but if your issue with Go is lack of non-local dynamic control transfer then good news: you're mis-informed and Go will be great for you!
- prodigal_erik 10y agoIt's not just up front. You only have to write the tedious and repetitive code once, but you have to keep reading it forever, because the code you care about is scattered here and there in the middle of it.