5 ms·
>In C#/Java, exceptions are often (ab)used to create invisible control flows through the program that are extremely difficult to follow I hear this often from
by ubertaco 3y ago
>In C#/Java, exceptions are often (ab)used to create invisible control flows through the program that are extremely difficult to follow
I hear this often from Go advocates, but as someone who cut my teeth in Java in 2004, I've only ever seen this done in one codebase: an old streaming parser that I haven't seen since, and where the alternatives were, at the time, generally _more_ confusing than tossing an "unexpected end of input" exception that contained context.
In the 19 years since (14 of which have been my professional career, mostly with Java as part of the job _somewhere_), I've never seen it since.
In the meantime, though, I've run into better, more-explicit error-handling strategies (monadic errors, union types with explicit unwrapping) that manage to make the usually-bad option ("I'm swallowing this error") explicit and intentional, which is not something Go manages to achieve. (Interestingly, these better approaches all pre-date Go, so it's not like there wasn't a better state of the art to learn from.)
But Go also doesn't make error-propagation easy either.
Somehow, Go manages to optimize for accidentally swallowing errors, which is sort of impressively bad. It reminds me of something like INTERCAL: engineered to be as much a footgun as possible. INTERCAL is a parody, though, so it has that going for it.
- slantedview 3y ago> Somehow, Go manages to optimize for accidentally swallowing errors How so?
- josephcsible 3y agoBecause if you forget to write the code to account for an error, your program still compiles (unlike languages that use sum types for errors) and then said error will be accidentally swallowed when it occurs (unlike languages that use exceptions for errors). The only other languages I can think of that are as bad as Go about this are C and assembly.
- koito17 3y ago> unlike languages that use exceptions for errors I present to you Sneaky Throw, credit to the author in [1] public class Sneak { public static RuntimeException sneakyThrow(Throwable t) { if ( t == null ) throw new NullPointerException("t"); Sneak.<RuntimeException>sneakyThrow0(t); return null; } @SuppressWarnings("unchecked") private static <T extends Throwable> void sneakyThrow0(Throwable t) throws T { throw (T)t; } } Now you too can force your callers to accidentally swallow errors and have the code compile ;) [1] https://www.mail-archive.com/javaposse@googlegroups.com/msg05984.html https://www.mail-archive.com/javaposse@googlegroups.com/msg0...
- josephcsible 3y agoThat lets you subvert checked exceptions, but the sneakily thrown exception will still be thrown and bubble up, not silently ignored.
- ubertaco 3y agoLooking at some other options for comparison: The much-maligned checked exceptions, obviously, _require_ you to have a "catch" block for the exceptions in question, or else you get a compiler error. Option types, Result types, and Either types (which are just generalized Result types) _require_ you to unwrap them explicitly, or else you'll get a compiler error because a Result<T> is not a T. In Haskell, you've got monadic error-handling inside of do-notation, which is implicit, but at least does the right thing by default of propagating the error back to you, rather than defaulting to swallowing it and moving on. Meanwhile, in golang, you write this form around 6-7 times in any function of more than a few lines: result, err := someFunctionCall(input) if err != nil { return nil, err } ...sure, that's so much noise it's hard to miss.....the first time. But since that's literally the only way errors can be handled, you wind up with something more like this: request, err := readHttpRequest(inputStream) if err != nil { return nil, err } userSubmission, err := parseUserSubmission(request) if err != nil { return nil, err } err = validateUserSubmission(userSubmission) userSubmission = formatAndTruncateMessageText(userSubmission) submissionTimestamp, err := clock.currentTimeMillis() if err != nil { return nil, err } insertedId, err := saveUserSubmission(userSubmission, submissionTimestamp) return formatResponse(userSubmission, insertedId, submissionTimestamp) ...how quickly can you spot the error that was swallowed? How quickly could you spot it at 2am when another, downstream service is broken because its submissions are failing validation but the validation error isn't propagated? By taking away the typesafety of requiring some sort of type wrapper for multiple return that must be unwrapped, _and_ by taking away the enforcement that you have to check for and either propagate or explicitly swallow the error (by way of a result type or even checked exceptions), Golang takes away your guardrails, leaving you on the mountainside and liable to fall off easily. By making you do repetitive boilerplate "if err != nil { return nil, err }" every other line or so (rather than providing automatic error-propagation machinery like Haskell's do-notation or Rust's `?` operator), Golang lulls you into "highway hypnosis"[1], setting you up to be much more likely to accidentally drive over the cliff. It makes the Right Thing™ tedious, easily omitted, and only enforced by your own constant vigilance (or complex external tooling that has to guess at your intent), and makes the Wrong Thing™ the default. [1] https://en.wikipedia.org/wiki/Highway_hypnosis https://en.wikipedia.org/wiki/Highway_hypnosis