3 ms·
It's funny to me when people see screenfuls of stack traces and remark how clear and useful it is. Perhaps, but do you really need all that? At what cost to you
by teeray 1y ago
It's funny to me when people see screenfuls of stack traces and remark how clear and useful it is. Perhaps, but do you really need all that? At what cost to your logs? I'd much rather have a one-liner wrapped error that cuts through all the framework and runtime noise. Yes, I can trace just as effectively (usually better)--the wrapping is very greppable when done well. No, in over a decade of writing Go full time, I have never cared about a runtime function or the other usual verbose garbage in my call stack.
- rwiggins 1y ago> how clear and useful it is. Perhaps, but do you really need all that? Do I need clear and useful things? Maybe not. Would I like to have them anyway? Yes. Years ago, I configured a Java project's logging framework to automatically exclude all the "uninteresting" frames in stack traces. It was beautiful. Every stack trace showed just the path taken through our application. And we could see the stack of "caused-by" exceptions, and common frames (across exceptions) were automatically cut out, too. Granted, I'm pretty sure logback's complexity is anathema to Go. But my goodness, it had some nice features... And then you just throw the stack trace in IntelliJ's "analyze stacktrace" box and you get clickable links to each line in every relevant file... I can dream. > the wrapping is very greppable when done well Yeah, that's my other problem with it. _When done well._ Every time I write an `if err != nil {}` block, I need to decide whether to return the error as is (`return err`) or decorate it with further context (`return fmt.Errorf("stuff broke: %w", err)`). (Or use `%v` if I don't want to wrap. Yet another little nuance I find myself needing to explain to junior devs over and over. And don't get me started about putting that in the `fmt` package.) So anyway, I've seen monstrosities of errors where there were 6+ "statement: statement: statement: statement: statement: final error" that felt like a dark comedy. I've also seen very high-level errors where I dearly wished for some intermediate context, but instead just had "failed to do a thing: EOF". That all being said, stack traces are really expensive. So, you end up with some "fun" optimizations: https://stackoverflow.com/questions/58696093/when-does-jvm-start-to-omit-stack-traces https://stackoverflow.com/questions/58696093/when-does-jvm-s...
- teeray 1y ago> _When done well._ Every time I write an `if err != nil {}` block, I need to decide whether to return the error as is (`return err`) or decorate it with further context (`return fmt.Errorf("stuff broke: %w", err)`) Easy. Always wrap. Wrap with what you were doing when the error occurred. > I'm pretty sure logback's complexity is anathema to Go. But my goodness, it had some nice features... And then you just throw the stack trace in IntelliJ's "analyze stacktrace" box and you get clickable links to each line in every relevant file... I can dream. Yeah, despite the proliferation of IDEs for Go in recent years, Go has traditionally been pretty anti- big-iron IDE.
- tsimionescu 1y ago> Easy. Always wrap. Wrap with what you were doing when the error occurred. Then we're back to having stack frames for framework and runtime code in our error traces.
- teeray 1y agoIn my experience doing this, it does not. The wrapping is all yours to write—frameworks and the standard library don’t use it. Their errors would appear as “leaf” errors decorated by the wrapping you, as a team, have added.
- tsimionescu 1y agoPeople who write frameworks and even the standard library are still Go programmers. And all major projects have their own internal libraries and frameworks. So any Go programmer has to learn when it's better to wrap an error, and when it's better to bubble it up as-is. Or perhaps the people who wrote the Go standard library don't follow the ideal Go best practices?
- jitl 1y agoWhenever I get an error in NodeJS without a stack trace I am pretty pissed off about it. When my program is failing for some reason I really want to know where it came from, and the stack is hugely helpful in narrowing down the possibility space.
- tsimionescu 1y agoI'm not arguing that stack frames are as good as manually written error traces can be - they're clearly not. I am simply amazed that people include "generate error traces" as a form of "handling" an error. The argument for explicit error values is often something like "it encourages people to actually handle their errors, rather than ignoring them". And on the face of it, this has some merit: we've all seen code that assumes an HTTP request can't fail, and now a small timeout crashes the entire backup procedure or whatever. But if "handle the error" simply means "decorate it with a trace and return it", then exceptions already do this, then you're really admitting that there is no fundamental difference from a exception, because this is exactly what exceptions do, all on their own. Sure, they produce less useful traces, but that's usually a tiny difference. After all, the argument wasn't "you'll get better stack traces than exceptions give you", it was "people will be more careful to handle errors". This is also relevant, because if the goal is to get better error traces, that can also be done with exceptions, with just some small improvements to syntax and semantics (e.g. add syntax for decorating a call site with user supplied context that will get included in any exception bubbled from it; add support in an exception to only print non-library stack frames, add support in the language to declare certain variables as "important" and have them auto-included in stack traces - many ideas).
- teeray 1y ago> there is no fundamental difference from a exception Flow of control is obvious and traceable with explicit errors—they are not some “other” to be dealt with. Exceptions in many languages are gotos, except you don’t know where you are going to and when you might goto. Can this method fail? Who knows! What exceptions can be thrown by this? Impossible to say… better to simply `catch Exception` and be done with it.
- tsimionescu 1y ago> Flow of control is obvious and traceable with explicit errors—they are not some “other” to be dealt with. That's a different discussion entirely. And even so, whether any statement can terminate early should not be very relevant: that's why we have try-with-resources/finally/defer and other similar mechanisms. > Exceptions in many languages are gotos, except you don’t know where you are going to and when you might goto. No, they are not, in any language with exceptions except Common Lisp and Windows SEH. Exceptions in all common languages are early returns, they return to the calling function. Of course, if the calling function doesn't catch them, it will also return early to its calling function. Tracing where control flow will continue after an exception is thrown is exactly equivalent to tracing where control flow will continue after an error is returned in Go. > Can this method fail? Who knows! Java has checked exceptions to explicitly mark functions that can throw. While there were some problems with that, especially around the lack of generic exceptions support, this seems like the right way to go, and it mostly got a bad rep simply because of a different zeitgeist in programming at the time. > What exceptions can be thrown by this? Impossible to say… better to simply `catch Exception` and be done with it. This is exactly the same in Go, where every single function returns `error`, and there is very rarely any documentation to say what types of errors it might actually return; and virtually all Go code does "if err != nil", which is exactly the same as catch(Exception). Not to mention that most errors used in most Go code are fmt.Errorf, so they don't carry any type information to begin with.