It always shocks me how often people don't read stack traces and just check the error message on the last line. I've had several times where people have looked at me like a magician because I just read the stack trace and told them which line the error was one.
This. An internal web service I wrote (in Java) returns the full stack trace when something explodes. Stack traces are a killer feature. 95% of the time it's trivial to find the source of a problem. It's like a "light" version of debugging.
The main weakness is that they don't work as effectively for multithreaded applications because it's considerably harder to reconstruct state in those contexts. Nonetheless, they're still a better debugging tool than a watch variable or a print statement.
I suspect luck. Data can be subtly corrupted (especially with fancy transport protocols/libraries) yet you don't fail because checking every time you pass it around is too slow.
I treat data as data and nothing else. Not as executable code. Ok, I’m totally lying, but when I treat data as code, I’m ridiculously careful about it.
I think failing fast is definitely the key here, I've worked in codebases where everyone is coding super defensively because everyone else was coding defensively, and so you get errors not actually throwing until several steps away from where they were caused.
What kind of work you do has a huge impact on that, too.
I've never written code for an arduino or other microcontroller as a profession (but I do love to tinker with my arduinos). There are numerous problems I can experience in Arduino programming that I'll likely never ever see in my day job.
I've been writing programs in some capacity or another for about 25 years now I guess. There's still plenty of problems that are common for some types of prorgamming that I've never experienced.
What kind of work you do has a huge impact on that, too. [...] I was giving a code-pair interview the other day, and the candidate introduced a Stack Overflow error. I remarked in my notes how long it'd been since I'd seen a real Stack Overflow error in the wild. I personally haven't seen it in at least a decade, maybe more. But that's likely because I'm pretty well aware of situations where I find myself in an infinite loop.
Stack allocations can be nice, since they're automatically and quickly released when the function returns. Avoids heap fragmentation, especially in multi-threaded programs.
What many programmers don't know is how "tiny" stack space usually actually is.
Or cases where the stack gets corrupted... That can get "fun" trying to work out what caused it, especially when the code that corrupted the stack is nowhere near the code that faults due to the corruption.
and tail-call-optimized recursive code that smashes the stack flat.
a modern compiler will preserve stack frame information across inlining/loop unrolling, and tail call optimization.
I write some idiotically cursed code that explicitly abuses zero-sized-type-erasure as well as memoization to optimized generated runtime code.
I've seen gcc/clang emit idiotic amounts of dwarf data all pointing to a single mov because 4 function calls, the creation of a index-type, and the index operation were all optimized away at compile time. Throwing a panic in the underlying code gave me a correct stack trace.
It looks wonky when you single step through a fully optimized binary in a debugger, but that's why debug builds exist ¯\(ツ)/¯
yes, I 'm very old school, been doing C++ and Java since the nineties and now Python. Java got stack traces right. C++ was pretty crummy back in the day. Python is kind of OK but nowhere near as good or consistent as Java.
The one thing that I loved about Java and everyone else hated was checked exceptions. It was an excellent way of tracking in your code where you had to handle or propagate exceptions and it made code very explicit about it. Of course hipsters hated it because it was associated in Java and nowadays nobody does checked exceptions anymore. That's a big loss.
Checked exceptions are great. The main issue is that Java the language hasn't given the capability to uncheck those checked exceptions easily so people end up checking things they shouldn't instead of converting and throwing an unchecked exception. Swift does a really good job at this, they provide both try! and try? to either "uncheck" or convert an error to null.
The second issue is that Java the language has made checked exceptions useless with lambdas/higher order functions so a lot of devs reject it on that principle as well. Scala has done some experimental work to get that to work and I really hope is gets adopted long term in Java: https://docs.scala-lang.org/scala3/reference/experimental/canthrow.html
I'm highly skeptical of checked exceptions for the simple reason that if a language feature is a great idea lots of other languages copy it. Not happening for checked exceptions, and similarly classloaders.
Lots of languages have checked errors and lots of programmers have been praising them. Everyone’s favorite language, Rust, has checked errors. F#, OCaml, Gleam, Haskell, all have checked errors in the type system.
No shit. I said easily. Currently to uncheck a checked exception it requires way too much boilerplate. There's a huge difference between catching and rethrowing unchecked and having language syntax to do that easily
URI uri;
try {
uri = new URI("https://google.com");
} catch (URISyntaxException ex) {
throw new RuntimeException(ex);
}
vs:
var uri = try! new URI("https://google.com")
var uri = new URI!("https://google.com")
Yes, the issue of lambdas breaking checked exception is a big deal. Given the choice I'd take checked exceptions over lambdas. But that ship has sailed and Java as it stands is badly wounded by this inconsistency.
Checked exceptions are great otherwise. It validates your code logic via types.
Of course hipsters hated it because it was associated in Java
At the time the hipsters would've been big into dynamic languages I think, though I may be thinking more about the early aughts than the late nineties. I don't really know what the hipsters were into before Ruby.
and nowadays nobody does checked exceptions anymore
I'd say nowadays we have pretty much the same thing just with sum types. The difference between these two (that are kinda pseudocode to highlight the similarities)
A foo() throws B {
if problem {
throw B;
}
return A;
}
and
fn foo() -> Result<A, B> {
if problem {
return Err(B);
}
return Ok(A);
}
is just kind of flavoring. In either case the language will force you to deal with the case where you got a B explicitly. What's lacking in a type system with sum types are the unchecked exceptions.
Python is also slightly older than Java, so you could say they're both children of the 90s when it comes to some of their design choices—though of course they've changed a lot over 30 years of general availability.
I can't say now. But in the nineties the chaining of stack traces had to be implemented manually, not to mention exceptions in C++ were a novelty then and most frameworks didn't make good use of them. I'm sure things are very different now.
Checked Exceptions were the right approach but for various reasons largely to do with the rest of Java became less used. Most of it has to do with java.util.function classes not having the exception parameterized (which you can do if you make your own abstract single method interfaces).
That is java.util.function.Function should be:
public Function<T,R,E extends Exception> {
R apply(T) throws E;
}
However I think this makes type inference for working with Stream difficult and Java also lacks higher kinded types.
It is notable that Checked Exceptions are very similar and are a subset of an Effect system. Effects are a newer programming language model that allows you do all sorts of things including exceptions.
I'll be honest, having gone from mostly Java backend work to mostly Kotlin on the JVM backend work over the last 5 years, I haven't missed checked exceptions once.
I don’t agree with this. I say fail fast and be idempotent.
Let all exceptions bubble up and get logged however possible and assume that the issue is transient (it almost always is). Have a very simple class(es) that handles tearing down the entire object model between units of work and creating it for the next UOW.
Size your UOW correctly to handle tearing down and spinning up the object model without tanking performance, but if you’re writing your constructors correctly, this isn’t even much of an issue. The thread pool remains. The connection pools remain. Set your service to fail fast if there are any uncaught exceptions (looking at you async/await), and set it to restart automatically.
Yes, I’m a backend developer, but much of what I say here holds true. The last thing you want is your program to keep running in an invalid state, and even error handling code can have errors. My error handling code is so short (because of the aforementioned practices) that it’s almost impossible to fuck up. And if you handle all exceptions the same way, you can get a little less simple and handle logging in cases when the network/filesystem/email/database is down.
I mostly follow your pattern as well but at times where you can handle exeptions (like retry a third party service or something) I find it useful to know what to expect. Like handling the socket error with a retry but let the configuration error bubble up.
Checked exceptions felt like the perfect balance. Go and its errors is way too unwieldy. Grouping error handling in an appropriate manner in Java just feels so much better to me. I definitely have far more experience with Java so I’d be curious if there’s a better way for go but assume not.
Reading error messages and stack traces and actually working to comprehend them is one of the biggest differences I see between junior and senior engineers.
Most senior engineers can read them, understand and take action. Many juniors seem to skip reading or just skim them and go straight back to the code, where they struggle to figure out what is wrong. It is definitely an important and underappreciated skill.
This thread is making me feel like I'm losing my mind. Who is not reading stack traces? This literally feels on the level of a writer being like "the biggest difference between junior and senior writers is paying attention to the spellchecker, most senior writers can notice when the spellchecker says a word is misspelled and take action, while juniors will just skip over them and keep editing." Like a thing that is explained to you once and you immediately understand as a core tool for doing your job.
Depends a bit on the stack trace. I tend to skim for interesting bits. In a generic Python stack trace that's easy enough. Given a Java stack trace though, I'm more likely to see if I can't avoid having to penetrate that denseness. And if it's been passed through some log shipper that's broken every line into a separate log message I'm not going to try to piece it back together (but the logshipper configuration should be fixed).
This is kind of similar to the responses you'll get from various compilers. E.g. rustc will generally produce very nice error messages; while some other compilers can produce some rather incomprehensible output. Programming Rust (preferably with a language server like rust-analyzer) you'll get a lot of feedback, but most of it is easily actionable like "you forgot a semicolon here" or "try cloning" or "try adding a & to make this a reference".
If you train people to expect that error messages are dense and incomprehensible, they'll stay away from them. And at some point people will think that maybe it'll be easier to get an industrial electromagnet to find that damned needle in the enormous haystack.
I can see how it can be confusing at first, because oftentimes where the stack trace dies is deep inside some obscure operating system function, when the actual cause is somewhere halfway up the stack before it leaves your code.
it's a bit like forgetting a ; in C, you end up with 50+ crazy errors and warnings that make it look like everything is fucked until you scroll up to the first one and realize what the real problem is.
I completely agree with you. I also totally relate to what everyone is saying. It might be different if you work at a big tech company, but in the places I have worked basic competence in coworkers is far from guaranteed.
I’ve seen some questions on the various learning and language subs where they just paste the “gcc exited with error 1” error or whatever, and then ask how to fix their program. I get not understanding an error message, but somehow people aren’t lexing any of the info in front of them.
I’m betting part of it is the lack of experience with terminals—everybody wants an IDE where one click will build (correctly? no telling) and run their program, but that’s a shit way to learn, it can swallow diagnostics, and when it breaks (it will) they’re lost.
Often, that last line is an error resulting from the real error as well. So hard to help when presented as a screenshot of the last line in a Teams chat.
"Modern" languages with value-based error handling expect users to create custom error types for each use case instead of inheriting from some base Exception class. So an error has a stack trace only if the programmer explicitly added it there. IDK about Go but Rust has libraries with error types that do this for you.
One the reasons for that is probably performance. Stack traces are expensive to create, especially in languages that compile to native code (AFAIK it's cheaper in languages that already use VM/interpreter).
What about getting stuff like function names and line numbers though? It needs to be logged for stack traces to be useful. I know you can technically do that after the fact if you have an original binary with debug symbols (which you may not have in case of e.g. open source software) but it will make studying the logs when debugging something much harder.
Collecting the stack trace in general is expensive, this is why for some exceptions in HotSpot it starts to disable stack trace collecting. You need to consider though that you only pay the cost of this when you actually encounter errors. With value based approaches you pay the cost of checking for errors on every function call.
is it only expensive when you have to actually throw the exception (or "print" the stack trace somewhere), or is it also expensive to "keep track of it" too?
Never throwing has no cost. Throwing is actually very cheap. The cost is filling in the stack trace. If you override fillInStackTrace to return nothing it should perform just like a normal return. If I remember correctly from a video I watched Odersky said that’s how Scala is actually implementing their breaks feature.
Afaik exceptions are cheaper on the happy path because you need less code and don't need branching on checking the results of methods that can fail. Throwing an exception is kind of an escape hatch that leaves the normal path and travels a seperate path until it finds a catch or terminates your program.
I've had significant issues in Java due to exceptions being costly to initialize. They're a custom exception type from my company though; so I am unable to speak generally.
But yes, exception initialization can be surprisingly costly!
It's possible to get them but most people don't because it's useless (in contrast to what this article suggests). In reality reading a stacktrace is much harder than a human readable error with context added in.
Stack traces are long, full of noise and function calls that you don't care about. Will miss important info like which id caused the error, ... And if you have multiple threads or when you rethrow exceptions it will become even worse.
You can have, but it's not needed so usually people don't add it, simple as that. You think it's always needed probably because you always use those languages and never had good error messages with a lot of context.
Just think about the samples in the article:
sqlite3.OperationalError: Database file "/home/piku/.piku/data/wikdict/dict/en-ko.sqlite3" does not exist
How does the stack trace attached is gonna help you? Probably you had a mistake in the DB address. Is the stracktrace showing you where did you set the address? No. It's just noise.
I have worked for years in different companies with Go, I can't recall a time saying wow if I just had stacktrace.
What are you doing then? If you’re thinking “how did the code get into this situation” then a stack trace could be the most direct answer to that question than you’ll get anywhere else, supplemented by logs and metrics.
We’ve hired a bunch of Go devs who didn’t use stack traces before they arrived who now do daily and couldn’t live without them. It’s possible you are just this person, especially as you seem to be a person who hasn’t used stack traces with Go, rather than someone who has and found them not to be useful.
How does the stack trace helps you find where you set the DB path? It's in a totally different place. Just open the article and read the stacktrace, does it help?
I used exceptions with stacktraces extensively in other languages and I think Go is superior by a good margin. Second comes things like Sentry+Python that can extract very good context out when an error happen like the value of local variables at that moment. There's a reason the default is like that.
You can look at the stacktrace alongside the code and see where the variable that went wrong came from, and see where the bug in the code left that variable empty when it should have been set.
There are lots of errors that are ‘expected’ such as validation errors. They aren’t useful to have a stack trace for.
But any error that is internal to your program and caused breakage for a user flow it is really useful to have a stack trace for. That’s exactly why crash reports include core dumps and why Go itself has stack traces attached to panics, the only reason it doesn’t have them attached by default to errors is because of a trade-off for performance against the errors-as-values philosophy that would mean errors are created a lot more than they are in other languages, which makes the cost of capturing the trace more substantial.
But in most of the Go code written, where it’s your average web app, that is not a concern. So you may as well collect them and be better off for it.
Yeah it’s one of (imo) the worst design decisions of a language that has been adopted for a lot of backend web dev work.
You can fight to get them back though, and I’ve done that successfully in our job: https://incident.io/blog/golang-errors
Seeing the surrounding context and call stack going into the error is probably the most important debugging context you could have when something goes wrong. I know why Go decided against it by default but I don’t think that rational applies well to what the majority of Go code is used to build today.
What we call errors in Go are just values like any values.
For example EOF is an error and will never need a stacktrace.
We often use custom error like UserNotIdentified, ObjectNotFound, and so on that really doesn't need any stacktrace of course.
But when there is a real runtime error it panics and there is a traceback.
For this kind of error you don't need a stacktrace because you handle it immediately.
For example EOF you stop to read. ObjectNotFound you create it, UserNotIdentified you redirect to login page, or you return it and the caller will know how to handle it.
For programming error like a null pointer it will panic and in this case you have a stacktrace like in any language.
It's more like the exit status of a linux command.
For this kind of error you don't need a stacktrace because you handle it immediately.
But it's not about handling. No one lets any exceptions, errors, whatever bubble up and halt the execution. There are errors that you can handle perfectly well and let the application gracefully continue, but still might want to know what lead to the error in the first place
I think Go is simply the wrong language to use if you want graceful continuity on error. I specifically use it because it's easy to have things bubble up and stop execution.
Most of the systems I've worked on needed to be correct more than they needed to be available.
In Go, while you don't have stack traces by default, you have wrapped errors.
So you'd often see something like:
failed to cancel order 123: error loading order history for user 456: failed to query rows: statement timed out
Which is much more useful than a stack trace. Now I effectively have a stack trace, but with custom messaging at each point and including the relevant data I'd need. When working with a production app in node, often I'd have to find the error, then figure out the context, then use that to understand what's happening. In Go on the other hand, if done well then the errors often contain enough context to figure out the problem on their own.
Neither of the places I've worked that use Go in production have had this issue. People use fmt.Errorf (or previously, errors.Wrapf). At some point you have to trust your developers to do the basics, just like you have to trust them to check the error in the first place.
Maybe I'm lucky in the places I've worked, but I've never had a situation where the error wasn't enough to immediately see what's going on and where.
So the answer to replacing an age old, war tested feature that other languages get for free is to manually replicate it at every single point an error could happen? And they call Java verbose...
Don't even get me started on the insanity of stringly typed errors.
Well user code knows where it gets the error and has to decide how to respond to it. It can print a stack trace if it wants to. This is opposed to other languages where functions can throw whenever or wherever they want and you’re not forced or even expected to catch, so user code would normally have no clue that an error has happened.
The code that knows "where it gets the error" won't know where the error comes from. If you print the stack trace right there, it only get to the point where it "get" the error, not when the error is generated.
Specifically in this comment chain, I'm refering to go where an error does not contain a stack trace by default. From your description, that is at least an option in Rust (but you possibly still need to trust the library authors to do the right thing?)
I won't deny it, but if there's a stack trace attached to the error then at least you can work back to where the problem is. That has proven useless on some occasions, but useful in others.
Go's direction of "error is just a string" meaning that if the engineer write shit error message (let's face is, most of us do, me included) then you're SOL
Stacktraces are meant for developers, not for end users.
If an app shows a stacktrace, it is because an app has a bug and it crashed.
Showing a stack trace due to a user error (like file not found) is a symptom of sloppiness of the developers of the app. A user error should end up with a human readable message explaining what to do differently and stack traces irrelevant in this situation.
I don't know about Go, but Rust does give you a stacktrace if the app crashes due to a developer bug.
It does not give you a stacktrace when a user error is encountered, but in that case you shouldn't need a stacktrace; you need a nice error message.
What goes in the log and what goes to the UI are not the same thing. A performance hit is a valid reason not to have them, but that doesn't make them a silly luxury for the lazy.
"It is a poor tool that blames its craftsman." -- Franklin Benjamin
There is not much point in keeping stacktraces for the errors the app handled, even in the log. Stacktraces are to help developers figure out stuff they haven’t expected but which happened. Like accessing arrays out of bounds.
As a user I don’t care about your language as long as the app behaves correctly. Printing a stack trace after I gave it incorrect parameters, which is often a thing Python apps do a lot, is just wrong.
You definitely should do both. If the server responds with 500, the user should get a proper error page with as much info as you might have. The server error should also be monitored and logged.
Showing a stack trace due to a user error (like file not found) is a symptom of sloppiness of the developers of the app. A user error should end up with a human readable message explaining what to do differently and stack traces are irrelevant in this situation.
Could arguably be sloppiness, but stacktraces are nice when diagnosing issues with Minecraft modpacks, where sometimes one mod will cause conflicts with another mod, and neither one are particularly at fault, they're just not compatible with each other.
There’s a performance penalty for throwing exceptions at all in most languages (maybe all?). But at the point of an exceptional error I don’t care about performance and would prefer the debug info. The corollary is you shouldn’t use exceptions for expected and normal errors.
There’s typically a way to check for a file with a method that returns Boolean so id recommend doing that first, then a file not found is truly an exception.
How does that cause a race condition? There’s a small chance the file was deleted between checking and accessing, not that chance still exists even if you only access. Also unless you have a system where files are constantly being deleted for some reason this situation is highly contrived.
You’ve described a race condition. Because the file can disappear between the check and the use, the check is useless. The check doesn’t tell you anything if you want to open the file anyway. The correct way is to try open the file and handle the low level OS error (however it is signaled in your language - error code, error object, exception, etc).
That’s not a race condition. A race condition would be some process or thread trying to access the file before it was created but expecting it to have been created. If you are working in a system where to regularly expect a file access to fail, then it’s an optimization. If you don’t expect failure then really the expecting is fine since it’s should rarely happen.
I can’t speak to Go, but I imagine it’s somewhat similar to Rust in that the equivalent to “throwing an exception” does print a stack trace. It’s just that because that has a penalty that most errors don’t throw exceptions and instead return errors.
Please fucking tell this to a colleague of mine, dude has written more throw statements in in one year than I have in my entire life. He's stubbornly opposed to anything resembling Either.
All that makes sense. You can definitely roll your own exception handling in a language like c++ to get it to print out a stack trace. If you can use a debugger, it's easier to just have the debugger do it as the logic is already there.
I don't think that you could get it to work with libraries that you didn't yourself write.
There's also the small penalty of compiling in all the code for that stack trace. For example, if you weren't previously linking against some string library, now you are.
It gives you a stack trace if there is a panic, which is basically an exception, but by default errors are values. You can get a stack trace if you so choose to though
Because in highly concurrent code stack traces are useless. Plus, they’re expensive. So the default is to chain errors manually to get a logical callstack that can span many threads or even processes and still be useful. If you need, you can always use a library that will capture real stack traces.
regarding Rust or Go: if you expect a stack trace from a Result just because it returned Error, it's wrong...
there are two types of errors in programming: expected errors and unexpected ones. only the latter should throw exceptions / panics with a stack trace.
expected errors should be handled accordingly as nearest as possible.
like, if I have a list and I ask to possibly get the 3rd element, I receive an Option which I then handle according to the business logic I need.
or like, if I try to find some value given a predicate or a key, the map shouldn't throw an exception since it's normal that a value can be missing...
this is different from python where there are a lot of thrown exceptions for standard logic flow, for example...
or like, if I try to find some value given a predicate or a key, the map shouldn't throw an exception since it's normal that a value can be missing...
this is different from python where there are a lot of thrown exceptions for standard logic flow, for example...
Yeah, it depends on the type system available, really. Checked exceptions are kind of like a union type, only you wind up writing it A foo() throws B rather than foo() -> Either<A,B>, but the information and semantics are pretty much the same, you just need some different bits of syntax to handle it.
So we wind up with:
Cases where you always have a variable available, but the contents may be garbage and you need to check another variable to see if the first is actually usable:
map.get taking a buffer and the return value indicating whether the entry was found or not (I think most of us used to modern languages would think this is a weird and painful way of doing it)
map.get returning a tuple indicating the return value and the indicator return above, which is possibly a bit less weird; but both will let you proceed with a variable that contains a wrong value if you didn't check the indicator value
Cases where you get a variable XOR something indicating its absence:
map.get returning a value if found or throwing an exception if it was missing: Annoying for people who don't like exceptions but will not permit the use of a missing value
map.get returning a union type either containing the desired value or something indicating its absence, which you'll likely unpack with a match or something.
Personally I favor Python's foo.get(bar) over foo[bar] because I use foo.get(bar, baz) the way I'd use foo.get(bar).unwrap_or(baz) or something similar in Rust (see also: the entry api). But in any case neither language lets you proceed with a garbage value, which I think is the correct approach versus producing a potentially garbage value and an indicator value.
The exception style is just kind of flavor, but it is kind of annoying that especially with unchecked exceptions like in Python, you can get a surprise crash, as if you'd unwittingly used .unwrap() in Rust.
older languages, basically, resorted to exceptions for almost everything even when they shouldn't. newer languages evolved from that.
… Except the ones that use the double value style, like Go.
I was porting my hydrodynamics solver from python to C this week, and out of boredom I wrote my own stack trace. It literally only took 500 lines of code and I can raise any exception I want as long as I wrap all my functions with a macro. This made debug in C so much easier.
You can attach a debugger to those too and just wait for the application to crash and the debugger to trip. From there on you can copy the state to your machine and inspect the trace of where it happened locally.
You don't remote debug. You attach the debugger locally and only analyze the crashdump remotely (or do that locally as well). You can just dump the stack frames which will exclude data in heap memory.
We've attempted to do that before. It took forever just to get the core dump and then even when we did the tooling was so slow and painful to use. We never got any helpful information out of it. Ancient enterprise monolith apps are a bitch sometimes.
Yeah I guess you could deploy code with debug symbols, attach a process and manually transfer the dump, but I don't know under what circumstances I'd prefer that to just logging traces.
You don't need to deploy it with debug symbols. You can keep them in a separate file, as is common on Windows for example. You may have seen them in the form of .pdb files
In this situation you would generate a core file with for example assert() and you could find the stack from the core file with a debugger. And so you'd run your program normally which means no slowdown. Not that running under a debugger slows anything down since the debugger does not interfere with the executable unless stopped. But core files are not user friendly and take up way more space than just producing a stack trace yourself.
And my program needs to be fast so I don’t want to use debugger.
What kind of debugger are you using that slows down your application so much that one might consider not using a debugger? Debuggers usually have almost no impact on the speed at which your program executes because it's mostly left to execute as-is. The debugger only attaches itself to the handlers that get called when the application crashes, and only then will it walk the stack trace and perform other expensive operations.
The macro is simply used to check the return value of each function, and stack the traceback message when there is any errors. I need to do this for every function anyways.
Actually, you can emit SIGINT to achieve the same as an exception and see the stack trace if you run the program with a debugger (if the program was compiled in debug mode). You can use the core dump if you want to avoid a debugger.
an error message telling you exactly what went wrong (and how) and nothing else, is better than a long and ugly one with tons of useless information. and most stack traces are ugly (i suspect that’s why many people don’t read them)
This is a rather narrow view. Yes, in most stack traces, about 80-90% of the information isn't useful for your current problem. However, the 10-20% of information in a stacktrace that is useful is massively valuable. Removing 100% of the stacktrace because it contains 80% noise is the definition of throwing out the baby with the bathwater.
Unfortunately, no one seems to have figured out yet how to remove the 80-90% of useless information without also throwing out (a good chunk of) the 10-20% of information that we're actually interested in. And when the choice is either no stacktrace or a noisy but useful stacktrace, then the latter is most definitely the way to go.
There are attempts to remove the noise in certain tooling. We are a Java shop that has moved to DataDog. Under the errors section, there will be collapsable sections of hidden frames. It seems like it assumes you don't need a lot of framework lines, but you can open them up. It's not perfect, and it's not particularly useful for me, but it can be helpful for our juniors and customer operations.
Traditional stack traces become quite a bit less useful in languages with coroutines, like Swift and Kotlin, since the stack only goes up to the continuation point of the coroutine.
You need coroutine-aware context tracing for those languages; Swift has backtrace_async() for that, but very few stack trace tools and libraries use it.
C# can be somewhat hit and miss for this. Technically it's unnecessary and not really recommended to "await" in every call up the stack if you're just passing a task, but anywhere you just pass a child task up is also technically a place that blips out of the eventual call stack if an exception does happen.
IIRC Java solves this at language level by using green threads which automatically preserve stack traces across suspension points. Though they are not quite as ergonomic as Kotlin's coroutines yet. Structured concurrency is very verbose and I don't think there is a good alternative for Flow (async streams).
Yeah, most challenges I've seen with Java stack traces are the enormous proliferation of promises / futures. (This is also true in environments that desugar continuations into promises, like JavaScript with async/await, and Kotlin/Android).
It's very difficult to know who called what with an enormous chain of promises 68 levels deep in a stack trace.
Wow, I genuinely thought we all lived or died by them when diagnosing prod issues. Who thinks they're actually underrated? They're the single most useful thing in a log.
I don't see any reasons against having stack traces. Collecting the traces can cost some performance
Performance is the core reason, actually.
Yes, you could turn stack-traces off in Release mode to regain performance, but then... you lose stack traces! Where you need them most.
It's easy enough to run a program under a debugger in local, where not only you get the stack (back)trace, but you also get the inspect the various stack frames to figure out how you landed there in the first place. That's fairly impopular in production, though.
The really issue is that whether a failure is an error is contextual. Think look-up in a map, for example: should the map collect a stack trace, and annotate it with the missing key?
Well, if the key was expected to be present, it'd be nice. But perhaps it was just a check "en passant" and there's a perfectly good path to handle the look-up failure, in which case it's just a waste of time.
Well, these values are both statically and dynamically distinct from non-error values, so they're more like exceptions (which are also values). It's just that the language requires explicitly acknowledging these exceptions at the statement level and at every . Unlike Go or even Rust, Zig could have easily supported more implicit exceptions with a surface syntax change (i.e. make try implicit, which the compiler could easily do). So I would say Zig has exceptions, only it's decided to require a more explicit handling of them.
Zig's errors are not dynamically distinct, its just an error tag being returned with the result, there is no runtime for errors. The implementation details are not really important anyways, semantically they act like values:
Yeah at the end of day this is just bikeshedding on the definition of return by value :)
What is considered a runtime also is very unclear in general. eg: is C's libc startup code really a runtime?
For me the concept of return by value is literaly returning errors like they are values. In Zig its the convention to do that + there is a special type that has guardrails so you dont ignore the error too easely and some other stuff.
I don’t understand why this is even a conversation. It’s like people who swear debuggers are unnecessary who are so ignorant that they don’t know what they don’t know.
It’s the same as people at work who don’t actually read logs or release notes and immediately ask for help when something breaks. It’s easier to declare something is broken or unnecessary than it is to understand something.
In C, on linux, besides getting a stack trace the usual way (by running in a debugger, or debugging a core file), you can get your own stack traces. Write a little function that prints out a stacktrace and aborts, and call it in those "never supposed to get here" spots in the code and save yourself the trouble of capturing a core file and cranking up the debugger.
Yeah, I think that's an editor problem to solve. Stack traces could show the first line, or the first and last, before collapsing into an expandable region.
Actually, note to self: go digging through settings later and see if there is something in there...
Stack traces are great when you’re not working with secure systems running signed builds that are built with optimizations on and all debug symbols stripped. There’s nothing quite as fun as doing a full clean and build of all the software on the system only to learn that the stack trace couldn’t be decoded and you can’t tell if it’s just some random failure or a corrupt stack. Yay I wasted a ton of time and got nothing to show for it.
moekakiryu@reddit
It always shocks me how often people don't read stack traces and just check the error message on the last line. I've had several times where people have looked at me like a magician because I just read the stack trace and told them which line the error was one.
Lisoph@reddit
This. An internal web service I wrote (in Java) returns the full stack trace when something explodes. Stack traces are a killer feature. 95% of the time it's trivial to find the source of a problem. It's like a "light" version of debugging.
gHx4@reddit
The main weakness is that they don't work as effectively for multithreaded applications because it's considerably harder to reconstruct state in those contexts. Nonetheless, they're still a better debugging tool than a watch variable or a print statement.
ShinyHappyREM@reddit
They also don't help as much in situations where the error creeps into the state (data) and the program doesn't stop.
Like an emulator where one out of hundreds of opcodes sets a wrong flag value and all you see is a blank screen.
GayMakeAndModel@reddit
Wow. I’ve been in development for decades, and I haven’t had this problem. Maybe it’s because I write everything to fail fast and be idempotent? Luck?
13steinj@reddit
I suspect luck. Data can be subtly corrupted (especially with fancy transport protocols/libraries) yet you don't fail because checking every time you pass it around is too slow.
GayMakeAndModel@reddit
I treat data as data and nothing else. Not as executable code. Ok, I’m totally lying, but when I treat data as code, I’m ridiculously careful about it.
ShinyHappyREM@reddit
As soon as you make program execution dependent on external data (could even be even environment conditions), you're treating data as code.
(technically this line counts as a bytecode interpreter)
Ecksters@reddit
I think failing fast is definitely the key here, I've worked in codebases where everyone is coding super defensively because everyone else was coding defensively, and so you get errors not actually throwing until several steps away from where they were caused.
GayMakeAndModel@reddit
I’ve never felt so understood
Deranged40@reddit
What kind of work you do has a huge impact on that, too.
I've never written code for an arduino or other microcontroller as a profession (but I do love to tinker with my arduinos). There are numerous problems I can experience in Arduino programming that I'll likely never ever see in my day job.
I've been writing programs in some capacity or another for about 25 years now I guess. There's still plenty of problems that are common for some types of prorgamming that I've never experienced.
ShinyHappyREM@reddit
Stack allocations can be nice, since they're automatically and quickly released when the function returns. Avoids heap fragmentation, especially in multi-threaded programs.
What many programmers don't know is how "tiny" stack space usually actually is.
nerd4code@reddit
Also continuation-based stuff where you don’t really have a stack, and tail-call-optimized recursive code that smashes the stack flat.
mallardtheduck@reddit
Or cases where the stack gets corrupted... That can get "fun" trying to work out what caused it, especially when the code that corrupted the stack is nowhere near the code that faults due to the corruption.
valarauca14@reddit
a modern compiler will preserve stack frame information across inlining/loop unrolling, and tail call optimization.
I write some idiotically cursed code that explicitly abuses zero-sized-type-erasure as well as memoization to optimized generated runtime code.
I've seen gcc/clang emit idiotic amounts of dwarf data all pointing to a single
mov
because 4 function calls, the creation of a index-type, and the index operation were all optimized away at compile time. Throwing a panic in the underlying code gave me a correct stack trace.It looks wonky when you single step through a fully optimized binary in a debugger, but that's why debug builds exist ¯\(ツ)/¯
arkuw@reddit
yes, I 'm very old school, been doing C++ and Java since the nineties and now Python. Java got stack traces right. C++ was pretty crummy back in the day. Python is kind of OK but nowhere near as good or consistent as Java.
The one thing that I loved about Java and everyone else hated was checked exceptions. It was an excellent way of tracking in your code where you had to handle or propagate exceptions and it made code very explicit about it. Of course hipsters hated it because it was associated in Java and nowadays nobody does checked exceptions anymore. That's a big loss.
vips7L@reddit
Checked exceptions are great. The main issue is that Java the language hasn't given the capability to uncheck those checked exceptions easily so people end up checking things they shouldn't instead of converting and throwing an unchecked exception. Swift does a really good job at this, they provide both try! and try? to either "uncheck" or convert an error to null.
The second issue is that Java the language has made checked exceptions useless with lambdas/higher order functions so a lot of devs reject it on that principle as well. Scala has done some experimental work to get that to work and I really hope is gets adopted long term in Java: https://docs.scala-lang.org/scala3/reference/experimental/canthrow.html
kanzenryu@reddit
I'm highly skeptical of checked exceptions for the simple reason that if a language feature is a great idea lots of other languages copy it. Not happening for checked exceptions, and similarly classloaders.
vips7L@reddit
Lots of languages have checked errors and lots of programmers have been praising them. Everyone’s favorite language, Rust, has checked errors. F#, OCaml, Gleam, Haskell, all have checked errors in the type system.
Worth_Trust_3825@reddit
Runtime exceptions already exist.
vips7L@reddit
No shit. I said easily. Currently to uncheck a checked exception it requires way too much boilerplate. There's a huge difference between catching and rethrowing unchecked and having language syntax to do that easily
vs:
arkuw@reddit
Yes, the issue of lambdas breaking checked exception is a big deal. Given the choice I'd take checked exceptions over lambdas. But that ship has sailed and Java as it stands is badly wounded by this inconsistency.
Checked exceptions are great otherwise. It validates your code logic via types.
syklemil@reddit
At the time the hipsters would've been big into dynamic languages I think, though I may be thinking more about the early aughts than the late nineties. I don't really know what the hipsters were into before Ruby.
I'd say nowadays we have pretty much the same thing just with sum types. The difference between these two (that are kinda pseudocode to highlight the similarities)
and
is just kind of flavoring. In either case the language will force you to deal with the case where you got a
B
explicitly. What's lacking in a type system with sum types are the unchecked exceptions.Python is also slightly older than Java, so you could say they're both children of the 90s when it comes to some of their design choices—though of course they've changed a lot over 30 years of general availability.
glaba3141@reddit
What's wrong with c++ stack traces?
arkuw@reddit
I can't say now. But in the nineties the chaining of stack traces had to be implemented manually, not to mention exceptions in C++ were a novelty then and most frameworks didn't make good use of them. I'm sure things are very different now.
agentoutlier@reddit
Checked Exceptions were the right approach but for various reasons largely to do with the rest of Java became less used. Most of it has to do with
java.util.function
classes not having the exception parameterized (which you can do if you make your own abstract single method interfaces).That is
java.util.function.Function
should be:However I think this makes type inference for working with
Stream
difficult and Java also lacks higher kinded types.It is notable that Checked Exceptions are very similar and are a subset of an Effect system. Effects are a newer programming language model that allows you do all sorts of things including exceptions.
SharkBaitDLS@reddit
I'll be honest, having gone from mostly Java backend work to mostly Kotlin on the JVM backend work over the last 5 years, I haven't missed checked exceptions once.
no_displayname@reddit
I also liked checked exceptions because you were forced to handle all the errors. Rust kinda brings that back but it feels laborious at times.
GayMakeAndModel@reddit
I don’t agree with this. I say fail fast and be idempotent.
Let all exceptions bubble up and get logged however possible and assume that the issue is transient (it almost always is). Have a very simple class(es) that handles tearing down the entire object model between units of work and creating it for the next UOW.
Size your UOW correctly to handle tearing down and spinning up the object model without tanking performance, but if you’re writing your constructors correctly, this isn’t even much of an issue. The thread pool remains. The connection pools remain. Set your service to fail fast if there are any uncaught exceptions (looking at you async/await), and set it to restart automatically.
Yes, I’m a backend developer, but much of what I say here holds true. The last thing you want is your program to keep running in an invalid state, and even error handling code can have errors. My error handling code is so short (because of the aforementioned practices) that it’s almost impossible to fuck up. And if you handle all exceptions the same way, you can get a little less simple and handle logging in cases when the network/filesystem/email/database is down.
no_displayname@reddit
I mostly follow your pattern as well but at times where you can handle exeptions (like retry a third party service or something) I find it useful to know what to expect. Like handling the socket error with a retry but let the configuration error bubble up.
GayMakeAndModel@reddit
Agreed that having what exceptions can be thrown in the API documentation is a plus.
creepy_doll@reddit
Checked exceptions felt like the perfect balance. Go and its errors is way too unwieldy. Grouping error handling in an appropriate manner in Java just feels so much better to me. I definitely have far more experience with Java so I’d be curious if there’s a better way for go but assume not.
kobumaister@reddit
That's a good idea, until you get in front of a 500 lines stack trace from Java Spring.
kuikuilla@reddit
Pfft, those are rookie numbers.
inter_fectum@reddit
Reading error messages and stack traces and actually working to comprehend them is one of the biggest differences I see between junior and senior engineers.
Most senior engineers can read them, understand and take action. Many juniors seem to skip reading or just skim them and go straight back to the code, where they struggle to figure out what is wrong. It is definitely an important and underappreciated skill.
anzu_embroidery@reddit
This thread is making me feel like I'm losing my mind. Who is not reading stack traces? This literally feels on the level of a writer being like "the biggest difference between junior and senior writers is paying attention to the spellchecker, most senior writers can notice when the spellchecker says a word is misspelled and take action, while juniors will just skip over them and keep editing." Like a thing that is explained to you once and you immediately understand as a core tool for doing your job.
syklemil@reddit
Depends a bit on the stack trace. I tend to skim for interesting bits. In a generic Python stack trace that's easy enough. Given a Java stack trace though, I'm more likely to see if I can't avoid having to penetrate that denseness. And if it's been passed through some log shipper that's broken every line into a separate log message I'm not going to try to piece it back together (but the logshipper configuration should be fixed).
This is kind of similar to the responses you'll get from various compilers. E.g.
rustc
will generally produce very nice error messages; while some other compilers can produce some rather incomprehensible output. Programming Rust (preferably with a language server likerust-analyzer
) you'll get a lot of feedback, but most of it is easily actionable like "you forgot a semicolon here" or "try cloning" or "try adding a&
to make this a reference".If you train people to expect that error messages are dense and incomprehensible, they'll stay away from them. And at some point people will think that maybe it'll be easier to get an industrial electromagnet to find that damned needle in the enormous haystack.
QuerulousPanda@reddit
I can see how it can be confusing at first, because oftentimes where the stack trace dies is deep inside some obscure operating system function, when the actual cause is somewhere halfway up the stack before it leaves your code.
it's a bit like forgetting a ; in C, you end up with 50+ crazy errors and warnings that make it look like everything is fucked until you scroll up to the first one and realize what the real problem is.
hachface@reddit
I completely agree with you. I also totally relate to what everyone is saying. It might be different if you work at a big tech company, but in the places I have worked basic competence in coworkers is far from guaranteed.
nerd4code@reddit
I’ve seen some questions on the various learning and language subs where they just paste the “gcc exited with error 1” error or whatever, and then ask how to fix their program. I get not understanding an error message, but somehow people aren’t lexing any of the info in front of them.
I’m betting part of it is the lack of experience with terminals—everybody wants an IDE where one click will build (correctly? no telling) and run their program, but that’s a shit way to learn, it can swallow diagnostics, and when it breaks (it will) they’re lost.
Liatin11@reddit
Especially if you're running things with a ton of packages or dependencies
Ok_Satisfaction7312@reddit
Lol. I used to suffer from this 15+ years ago. I learned to always read the stack trace carefully from start to end.
bart9h@reddit
or maybe from the end to the start
zzkj@reddit
Often, that last line is an error resulting from the real error as well. So hard to help when presented as a screenshot of the last line in a Teams chat.
dustingibson@reddit
Also saves a ton of time not stepping through the code again just to see what steps it took before getting to that error. It's all right there.
ForlornPlague@reddit
Go doesn't give you stack traces? Can you change something to get them? That's insane to me, why wouldn't anyone want that?
equeim@reddit
"Modern" languages with value-based error handling expect users to create custom error types for each use case instead of inheriting from some base Exception class. So an error has a stack trace only if the programmer explicitly added it there. IDK about Go but Rust has libraries with error types that do this for you.
One the reasons for that is probably performance. Stack traces are expensive to create, especially in languages that compile to native code (AFAIK it's cheaper in languages that already use VM/interpreter).
thomaskoopman@reddit
I always turn on
-ggdb
and have never seen performance degradation. Or do you mean stack traces without using a debugger?johan__A@reddit
No they dont have to be expensive to create, they can even be almost free. And you can turn them off for the final build.
equeim@reddit
What about getting stuff like function names and line numbers though? It needs to be logged for stack traces to be useful. I know you can technically do that after the fact if you have an original binary with debug symbols (which you may not have in case of e.g. open source software) but it will make studying the logs when debugging something much harder.
vips7L@reddit
Collecting the stack trace in general is expensive, this is why for some exceptions in HotSpot it starts to disable stack trace collecting. You need to consider though that you only pay the cost of this when you actually encounter errors. With value based approaches you pay the cost of checking for errors on every function call.
obetu5432@reddit
is it only expensive when you have to actually throw the exception (or "print" the stack trace somewhere), or is it also expensive to "keep track of it" too?
vips7L@reddit
Never throwing has no cost. Throwing is actually very cheap. The cost is filling in the stack trace. If you override fillInStackTrace to return nothing it should perform just like a normal return. If I remember correctly from a video I watched Odersky said that’s how Scala is actually implementing their breaks feature.
https://www.scala-lang.org/api/current/scala/util/control/Breaks.html
Mognakor@reddit
Afaik exceptions are cheaper on the happy path because you need less code and don't need branching on checking the results of methods that can fail. Throwing an exception is kind of an escape hatch that leaves the normal path and travels a seperate path until it finds a catch or terminates your program.
vini_2003@reddit
I've had significant issues in Java due to exceptions being costly to initialize. They're a custom exception type from my company though; so I am unable to speak generally.
But yes, exception initialization can be surprisingly costly!
null3@reddit
It's possible to get them but most people don't because it's useless (in contrast to what this article suggests). In reality reading a stacktrace is much harder than a human readable error with context added in.
Stack traces are long, full of noise and function calls that you don't care about. Will miss important info like which id caused the error, ... And if you have multiple threads or when you rethrow exceptions it will become even worse.
shared_ptr@reddit
You can have a nice error message and metadata AND a stack trace and be infinitely better off.
The first question you ask when you see the error with a value that seems impossible is ‘how?’ Which is when you immediately need the trace.
null3@reddit
You can have, but it's not needed so usually people don't add it, simple as that. You think it's always needed probably because you always use those languages and never had good error messages with a lot of context.
Just think about the samples in the article:
How does the stack trace attached is gonna help you? Probably you had a mistake in the DB address. Is the stracktrace showing you where did you set the address? No. It's just noise.
I have worked for years in different companies with Go, I can't recall a time saying wow if I just had stacktrace.
shared_ptr@reddit
When that error message says:
What are you doing then? If you’re thinking “how did the code get into this situation” then a stack trace could be the most direct answer to that question than you’ll get anywhere else, supplemented by logs and metrics.
We’ve hired a bunch of Go devs who didn’t use stack traces before they arrived who now do daily and couldn’t live without them. It’s possible you are just this person, especially as you seem to be a person who hasn’t used stack traces with Go, rather than someone who has and found them not to be useful.
null3@reddit
How does the stack trace helps you find where you set the DB path? It's in a totally different place. Just open the article and read the stacktrace, does it help?
I used exceptions with stacktraces extensively in other languages and I think Go is superior by a good margin. Second comes things like Sentry+Python that can extract very good context out when an error happen like the value of local variables at that moment. There's a reason the default is like that.
shared_ptr@reddit
You can look at the stacktrace alongside the code and see where the variable that went wrong came from, and see where the bug in the code left that variable empty when it should have been set.
There are lots of errors that are ‘expected’ such as validation errors. They aren’t useful to have a stack trace for.
But any error that is internal to your program and caused breakage for a user flow it is really useful to have a stack trace for. That’s exactly why crash reports include core dumps and why Go itself has stack traces attached to panics, the only reason it doesn’t have them attached by default to errors is because of a trade-off for performance against the errors-as-values philosophy that would mean errors are created a lot more than they are in other languages, which makes the cost of capturing the trace more substantial.
But in most of the Go code written, where it’s your average web app, that is not a concern. So you may as well collect them and be better off for it.
shared_ptr@reddit
Yeah it’s one of (imo) the worst design decisions of a language that has been adopted for a lot of backend web dev work.
You can fight to get them back though, and I’ve done that successfully in our job: https://incident.io/blog/golang-errors
Seeing the surrounding context and call stack going into the error is probably the most important debugging context you could have when something goes wrong. I know why Go decided against it by default but I don’t think that rational applies well to what the majority of Go code is used to build today.
kaeshiwaza@reddit
What we call errors in Go are just values like any values. For example EOF is an error and will never need a stacktrace.
We often use custom error like UserNotIdentified, ObjectNotFound, and so on that really doesn't need any stacktrace of course.
But when there is a real runtime error it panics and there is a traceback.
Pomnom@reddit
Why do you think this doesn't need any stack trace though. The moment the error can be returned from more than 1 place, you need a stack trace.
kaeshiwaza@reddit
For this kind of error you don't need a stacktrace because you handle it immediately.
For example EOF you stop to read. ObjectNotFound you create it, UserNotIdentified you redirect to login page, or you return it and the caller will know how to handle it.
For programming error like a null pointer it will panic and in this case you have a stacktrace like in any language.
It's more like the exit status of a linux command.
OMG_A_CUPCAKE@reddit
But it's not about handling. No one lets any exceptions, errors, whatever bubble up and halt the execution. There are errors that you can handle perfectly well and let the application gracefully continue, but still might want to know what lead to the error in the first place
gopher_space@reddit
I think Go is simply the wrong language to use if you want graceful continuity on error. I specifically use it because it's easy to have things bubble up and stop execution.
Most of the systems I've worked on needed to be correct more than they needed to be available.
Kapps@reddit
In Go, while you don't have stack traces by default, you have wrapped errors.
So you'd often see something like:
failed to cancel order 123: error loading order history for user 456: failed to query rows: statement timed out
Which is much more useful than a stack trace. Now I effectively have a stack trace, but with custom messaging at each point and including the relevant data I'd need. When working with a production app in node, often I'd have to find the error, then figure out the context, then use that to understand what's happening. In Go on the other hand, if done well then the errors often contain enough context to figure out the problem on their own.
Pomnom@reddit
If and only if your engs is vigilant at wrapping error every where without reusing the same message. Otherwise you lose the context again.
Call this the pomnom rule: the probability of
return err
approaches 100% on any go code base larger than 1k line.and if your engs remember to do this at every error, instead of, you know copy a
fmt.Errorf()
from ~~chatGPT~~ stackoverflow.Kapps@reddit
Neither of the places I've worked that use Go in production have had this issue. People use
fmt.Errorf
(or previously,errors.Wrapf
). At some point you have to trust your developers to do the basics, just like you have to trust them to check the error in the first place.Maybe I'm lucky in the places I've worked, but I've never had a situation where the error wasn't enough to immediately see what's going on and where.
Pomnom@reddit
Yeah you do. But just like why I have CICD servers, I'd rather that automation exists to avoid manual errors.
Also I work at a revolving door kind of a team, so this kind of thing becomes a major annoyance very quickly.
r1veRRR@reddit
So the answer to replacing an age old, war tested feature that other languages get for free is to manually replicate it at every single point an error could happen? And they call Java verbose...
Don't even get me started on the insanity of stringly typed errors.
bleachisback@reddit
Well user code knows where it gets the error and has to decide how to respond to it. It can print a stack trace if it wants to. This is opposed to other languages where functions can throw whenever or wherever they want and you’re not forced or even expected to catch, so user code would normally have no clue that an error has happened.
Pomnom@reddit
The code that knows "where it gets the error" won't know where the error comes from. If you print the stack trace right there, it only get to the point where it "get" the error, not when the error is generated.
Specifically in this comment chain, I'm refering to go where an error does not contain a stack trace by default. From your description, that is at least an option in Rust (but you possibly still need to trust the library authors to do the right thing?)
bleachisback@reddit
Yeah I guess this is a problem in every programming language, though.
Pomnom@reddit
I won't deny it, but if there's a stack trace attached to the error then at least you can work back to where the problem is. That has proven useless on some occasions, but useful in others.
Go's direction of "error is just a string" meaning that if the engineer write shit error message (let's face is, most of us do, me included) then you're SOL
coderemover@reddit
Stacktraces are meant for developers, not for end users.
If an app shows a stacktrace, it is because an app has a bug and it crashed.
Showing a stack trace due to a user error (like file not found) is a symptom of sloppiness of the developers of the app. A user error should end up with a human readable message explaining what to do differently and stack traces irrelevant in this situation.
I don't know about Go, but Rust does give you a stacktrace if the app crashes due to a developer bug.
It does not give you a stacktrace when a user error is encountered, but in that case you shouldn't need a stacktrace; you need a nice error message.
Empanatacion@reddit
What goes in the log and what goes to the UI are not the same thing. A performance hit is a valid reason not to have them, but that doesn't make them a silly luxury for the lazy.
"It is a poor tool that blames its craftsman." -- Franklin Benjamin
coderemover@reddit
There is not much point in keeping stacktraces for the errors the app handled, even in the log. Stacktraces are to help developers figure out stuff they haven’t expected but which happened. Like accessing arrays out of bounds.
i_ate_god@reddit
I think this mentality depends on the language.
Python views exceptions almost the same as it views if statements. Java views exceptions as actual exceptional circumstances.
segv@reddit
There's always that one guy that tries to use them for regular control flow though :/
coderemover@reddit
As a user I don’t care about your language as long as the app behaves correctly. Printing a stack trace after I gave it incorrect parameters, which is often a thing Python apps do a lot, is just wrong.
Orbidorpdorp@reddit
You definitely should do both. If the server responds with 500, the user should get a proper error page with as much info as you might have. The server error should also be monitored and logged.
Luke22_36@reddit
Could arguably be sloppiness, but stacktraces are nice when diagnosing issues with Minecraft modpacks, where sometimes one mod will cause conflicts with another mod, and neither one are particularly at fault, they're just not compatible with each other.
Successful-Money4995@reddit
There's a performance penalty to the stack trace, isn't there?
tooclosetocall82@reddit
There’s a performance penalty for throwing exceptions at all in most languages (maybe all?). But at the point of an exceptional error I don’t care about performance and would prefer the debug info. The corollary is you shouldn’t use exceptions for expected and normal errors.
coderemover@reddit
File not found or invalid application input are not exceptional errors. They are expected errors.
tooclosetocall82@reddit
There’s typically a way to check for a file with a method that returns Boolean so id recommend doing that first, then a file not found is truly an exception.
coderemover@reddit
That’s the incorrect way of doing this.
tooclosetocall82@reddit
How does that cause a race condition? There’s a small chance the file was deleted between checking and accessing, not that chance still exists even if you only access. Also unless you have a system where files are constantly being deleted for some reason this situation is highly contrived.
coderemover@reddit
You’ve described a race condition. Because the file can disappear between the check and the use, the check is useless. The check doesn’t tell you anything if you want to open the file anyway. The correct way is to try open the file and handle the low level OS error (however it is signaled in your language - error code, error object, exception, etc).
tooclosetocall82@reddit
That’s not a race condition. A race condition would be some process or thread trying to access the file before it was created but expecting it to have been created. If you are working in a system where to regularly expect a file access to fail, then it’s an optimization. If you don’t expect failure then really the expecting is fine since it’s should rarely happen.
coderemover@reddit
It’s not an optimization because checking costs the same as opening and failing.
bleachisback@reddit
I can’t speak to Go, but I imagine it’s somewhat similar to Rust in that the equivalent to “throwing an exception” does print a stack trace. It’s just that because that has a penalty that most errors don’t throw exceptions and instead return errors.
bludgeonerV@reddit
Please fucking tell this to a colleague of mine, dude has written more throw statements in in one year than I have in my entire life. He's stubbornly opposed to anything resembling Either.
Successful-Money4995@reddit
All that makes sense. You can definitely roll your own exception handling in a language like c++ to get it to print out a stack trace. If you can use a debugger, it's easier to just have the debugger do it as the logic is already there.
I don't think that you could get it to work with libraries that you didn't yourself write.
There's also the small penalty of compiling in all the code for that stack trace. For example, if you weren't previously linking against some string library, now you are.
Legitimate_Plane_613@reddit
It gives you a stack trace if there is a panic, which is basically an exception, but by default errors are values. You can get a stack trace if you so choose to though
cre_ker@reddit
Because in highly concurrent code stack traces are useless. Plus, they’re expensive. So the default is to chain errors manually to get a logical callstack that can span many threads or even processes and still be useful. If you need, you can always use a library that will capture real stack traces.
tesfabpel@reddit
regarding Rust or Go: if you expect a stack trace from a Result just because it returned Error, it's wrong...
there are two types of errors in programming: expected errors and unexpected ones. only the latter should throw exceptions / panics with a stack trace.
expected errors should be handled accordingly as nearest as possible.
like, if I have a list and I ask to possibly get the 3rd element, I receive an Option which I then handle according to the business logic I need.
or like, if I try to find some value given a predicate or a key, the map shouldn't throw an exception since it's normal that a value can be missing...
this is different from python where there are a lot of thrown exceptions for standard logic flow, for example...
syklemil@reddit
Yeah, it depends on the type system available, really. Checked exceptions are kind of like a union type, only you wind up writing it
A foo() throws B
rather thanfoo() -> Either<A,B>
, but the information and semantics are pretty much the same, you just need some different bits of syntax to handle it.So we wind up with:
map.get
taking a buffer and the return value indicating whether the entry was found or not (I think most of us used to modern languages would think this is a weird and painful way of doing it)map.get
returning a tuple indicating the return value and the indicator return above, which is possibly a bit less weird; but both will let you proceed with a variable that contains a wrong value if you didn't check the indicator valuemap.get
returning a value if found or throwing an exception if it was missing: Annoying for people who don't like exceptions but will not permit the use of a missing valuemap.get
returning a union type either containing the desired value or something indicating its absence, which you'll likely unpack with amatch
or something.Personally I favor Python's
foo.get(bar)
overfoo[bar]
because I usefoo.get(bar, baz)
the way I'd usefoo.get(bar).unwrap_or(baz)
or something similar in Rust (see also: the entry api). But in any case neither language lets you proceed with a garbage value, which I think is the correct approach versus producing a potentially garbage value and an indicator value.The exception style is just kind of flavor, but it is kind of annoying that especially with unchecked exceptions like in Python, you can get a surprise crash, as if you'd unwittingly used
.unwrap()
in Rust.… Except the ones that use the double value style, like Go.
cheezballs@reddit
Ehhhh not all modern languages differentiate. It's a pretty contentious part of the Java language, or used to be anyway.
wildjokers@reddit
Java has checked and unchecked exceptions.
Crazy_Anywhere_4572@reddit
I was porting my hydrodynamics solver from python to C this week, and out of boredom I wrote my own stack trace. It literally only took 500 lines of code and I can raise any exception I want as long as I wrap all my functions with a macro. This made debug in C so much easier.
AyrA_ch@reddit
Why not use an actual debugger? Unless you strip the symbols you should get a stack trace too
bludgeonerV@reddit
Production errors that are hard to reproduce?
AyrA_ch@reddit
You can attach a debugger to those too and just wait for the application to crash and the debugger to trip. From there on you can copy the state to your machine and inspect the trace of where it happened locally.
cheezballs@reddit
Nobody is allowed to remote debug our product apps for security reasons.
AyrA_ch@reddit
You don't remote debug. You attach the debugger locally and only analyze the crashdump remotely (or do that locally as well). You can just dump the stack frames which will exclude data in heap memory.
cheezballs@reddit
We've attempted to do that before. It took forever just to get the core dump and then even when we did the tooling was so slow and painful to use. We never got any helpful information out of it. Ancient enterprise monolith apps are a bitch sometimes.
bludgeonerV@reddit
Yeah I guess you could deploy code with debug symbols, attach a process and manually transfer the dump, but I don't know under what circumstances I'd prefer that to just logging traces.
AyrA_ch@reddit
You don't need to deploy it with debug symbols. You can keep them in a separate file, as is common on Windows for example. You may have seen them in the form of .pdb files
Crazy_Anywhere_4572@reddit
I was just trying to reproduce the try except, raise and traceback in python. And my program needs to be fast so I don’t want to use debugger.
double-you@reddit
In this situation you would generate a core file with for example assert() and you could find the stack from the core file with a debugger. And so you'd run your program normally which means no slowdown. Not that running under a debugger slows anything down since the debugger does not interfere with the executable unless stopped. But core files are not user friendly and take up way more space than just producing a stack trace yourself.
AyrA_ch@reddit
What kind of debugger are you using that slows down your application so much that one might consider not using a debugger? Debuggers usually have almost no impact on the speed at which your program executes because it's mostly left to execute as-is. The debugger only attaches itself to the handlers that get called when the application crashes, and only then will it walk the stack trace and perform other expensive operations.
MyCreativeAltName@reddit
Uhh honestly I rather just use a debugger..
cheezballs@reddit
For prod logs. Not everything is being caught by a dev.
bludgeonerV@reddit
That's great if you never deploy your code...
Crazy_Anywhere_4572@reddit
The macro is simply used to check the return value of each function, and stack the traceback message when there is any errors. I need to do this for every function anyways.
Setepenre@reddit
You could use something like backward-cpp instead or backtrace
no macro required.
bedrooms-ds@reddit
Actually, you can emit SIGINT to achieve the same as an exception and see the stack trace if you run the program with a debugger (if the program was compiled in debug mode). You can use the core dump if you want to avoid a debugger.
stomah@reddit
an error message telling you exactly what went wrong (and how) and nothing else, is better than a long and ugly one with tons of useless information. and most stack traces are ugly (i suspect that’s why many people don’t read them)
njharman@reddit
Is what Python stack traces do.
TarnishedVictory@reddit
That's like saying the vast majority of a map is useless for the one route you're looking for.
TiddoLangerak@reddit
This is a rather narrow view. Yes, in most stack traces, about 80-90% of the information isn't useful for your current problem. However, the 10-20% of information in a stacktrace that is useful is massively valuable. Removing 100% of the stacktrace because it contains 80% noise is the definition of throwing out the baby with the bathwater.
Unfortunately, no one seems to have figured out yet how to remove the 80-90% of useless information without also throwing out (a good chunk of) the 10-20% of information that we're actually interested in. And when the choice is either no stacktrace or a noisy but useful stacktrace, then the latter is most definitely the way to go.
gravteck@reddit
There are attempts to remove the noise in certain tooling. We are a Java shop that has moved to DataDog. Under the errors section, there will be collapsable sections of hidden frames. It seems like it assumes you don't need a lot of framework lines, but you can open them up. It's not perfect, and it's not particularly useful for me, but it can be helpful for our juniors and customer operations.
chefox@reddit
Traditional stack traces become quite a bit less useful in languages with coroutines, like Swift and Kotlin, since the stack only goes up to the continuation point of the coroutine.
You need coroutine-aware context tracing for those languages; Swift has backtrace_async() for that, but very few stack trace tools and libraries use it.
xeio87@reddit
C# can be somewhat hit and miss for this. Technically it's unnecessary and not really recommended to "await" in every call up the stack if you're just passing a task, but anywhere you just pass a child task up is also technically a place that blips out of the eventual call stack if an exception does happen.
equeim@reddit
IIRC Java solves this at language level by using green threads which automatically preserve stack traces across suspension points. Though they are not quite as ergonomic as Kotlin's coroutines yet. Structured concurrency is very verbose and I don't think there is a good alternative for Flow (async streams).
chefox@reddit
Yeah, most challenges I've seen with Java stack traces are the enormous proliferation of promises / futures. (This is also true in environments that desugar continuations into promises, like JavaScript with async/await, and Kotlin/Android).
It's very difficult to know who called what with an enormous chain of promises 68 levels deep in a stack trace.
vips7L@reddit
This is why they went with virtual threads. It was an explicit design goal.
thedragonturtle@reddit
Wait what? Ffs when did people stop reading stack traces? They're a godsend!
wellings@reddit
Is this... a topic of debate? I sure hope not.
fudini@reddit
I send my stacktraces straight to vim so I can navigate them with telescope plugin
Setepenre@reddit
First thing I do in C++; add something to give me the backtrace. 99% of the time, I don't need to fire up the debugger anymore.
cheezballs@reddit
Wow, I genuinely thought we all lived or died by them when diagnosing prod issues. Who thinks they're actually underrated? They're the single most useful thing in a log.
matthieum@reddit
Performance is the core reason, actually.
Yes, you could turn stack-traces off in Release mode to regain performance, but then... you lose stack traces! Where you need them most.
It's easy enough to run a program under a debugger in local, where not only you get the stack (back)trace, but you also get the inspect the various stack frames to figure out how you landed there in the first place. That's fairly impopular in production, though.
The really issue is that whether a failure is an error is contextual. Think look-up in a map, for example: should the map collect a stack trace, and annotate it with the missing key?
Well, if the key was expected to be present, it'd be nice. But perhaps it was just a check "en passant" and there's a perfectly good path to handle the look-up failure, in which case it's just a waste of time.
johan__A@reddit
Zig has stack traces for errors as values.
pron98@reddit
Well, these values are both statically and dynamically distinct from non-error values, so they're more like exceptions (which are also values). It's just that the language requires explicitly acknowledging these exceptions at the statement level and at every . Unlike Go or even Rust, Zig could have easily supported more implicit exceptions with a surface syntax change (i.e. make
try
implicit, which the compiler could easily do). So I would say Zig has exceptions, only it's decided to require a more explicit handling of them.johan__A@reddit
Zig's errors are not dynamically distinct, its just an error tag being returned with the result, there is no runtime for errors. The implementation details are not really important anyways, semantically they act like values:
pron98@reddit
That's the definition of being dynamically distinct.
Of course there is. That's how the stack traces are collected.
Exception types are just like that, too.
johan__A@reddit
Yeah at the end of day this is just bikeshedding on the definition of return by value :)
What is considered a runtime also is very unclear in general. eg: is C's libc startup code really a runtime?
For me the concept of return by value is literaly returning errors like they are values. In Zig its the convention to do that + there is a special type that has guardrails so you dont ignore the error too easely and some other stuff.
pron98@reddit
Of course! Having a (relatively for its time) rich runtime was one of C's marketing points when it was young.
dominodave@reddit
Stack traces are underrated? Man wtf is even going on out there anymore if ppl aren't looking at stack traces anymore
F54280@reddit
Wait until the new generation discovers core dumps...
ShinyHappyREM@reddit
Of course the ~~n00bs~~ whippersnappers don't know about core dumps.
Ancillas@reddit
I don’t understand why this is even a conversation. It’s like people who swear debuggers are unnecessary who are so ignorant that they don’t know what they don’t know.
It’s the same as people at work who don’t actually read logs or release notes and immediately ask for help when something breaks. It’s easier to declare something is broken or unnecessary than it is to understand something.
smcameron@reddit
In C, on linux, besides getting a stack trace the usual way (by running in a debugger, or debugging a core file), you can get your own stack traces. Write a little function that prints out a stacktrace and aborts, and call it in those "never supposed to get here" spots in the code and save yourself the trouble of capturing a core file and cranking up the debugger.
pragmatick@reddit
I absolutely hate seeing stack traces but when an error occurs I'd rather have one than a one-line error message.
genpfault@reddit
Error: No error
mypetocean@reddit
Yeah, I think that's an editor problem to solve. Stack traces could show the first line, or the first and last, before collapsing into an expandable region.
Actually, note to self: go digging through settings later and see if there is something in there...
TimeSuck5000@reddit
Stack traces are great when you’re not working with secure systems running signed builds that are built with optimizations on and all debug symbols stripped. There’s nothing quite as fun as doing a full clean and build of all the software on the system only to learn that the stack trace couldn’t be decoded and you can’t tell if it’s just some random failure or a corrupt stack. Yay I wasted a ton of time and got nothing to show for it.
gelatineous@reddit
They seem correctly rated as extremely important.
vini_2003@reddit
Stack traces are absolutely brilliant.