Is there perhaps a concrete illustration on how using exceptions causes technical debt, especially in a GCed language like Go?
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.
You can pass the error (or another) up the stack. The language is flexible.
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.