Why I Prefer Exceptions To Errors
Вставка
- Опубліковано 21 лис 2024
- Recorded live on twitch, GET IN
Article
cedardb.com/bl...
By: Philipp Fent | / philippfent
My Stream
/ theprimeagen
Best Way To Support Me
Become a backend engineer. Its my favorite site
boot.dev/?prom...
This is also the best way to support me is to support yourself becoming a better backend engineer.
MY MAIN YT CHANNEL: Has well edited engineering videos
/ theprimeagen
Discord
/ discord
Have something for me to read or react to?: / theprimeagen
Kinesis Advantage 360: bit.ly/Prime-K...
Get production ready SQLite with Turso: turso.tech/dee...
Calculating fibonacci recursively is a common stand in for some moderately complex task that will reach very high stack depths. You could substitute in parsing XML tags, or any other tree data structure, and the point would stand. If you use errors as values, then your error checking code is included in-line with the rest of your code, which means an if check and a short jump. This means your hot path code is bigger and fits in lower level caches less well. Exceptions use a long jump table stored externally to the local hot code. So all of the exceptional path code is _not_ sitting in your local caches. This makes the non-exceptional case run significantly faster, at the cost of making exceptional code marginally slower.
When you throw an exception, that throw statement _is_ a long jump, to the global exception dispatcher, which takes the current program counter as an index into the exceptional path table to determine where to go next. The where-to-go-next will be the stack cleanup for each frame between the current continuation and wherever your first catch is. Again, this is out of band to your regular code, so will need to be loaded into the L1 cache when an exception happens.
Um ... exceptions don't happen often, so that is a bit of a silly way to look at it. You want your no-exception case to be fast, not your exception case.
And what prevents the Rust compiler from doing that exact optimization when errors are hoisted with the `?` operator? Aka the "performance gain" is an implementation detail, not a fundamental property of error handling styles. Also who uses recursion in production server code? That's a terrible idea.
@@Tony-dp1rl I think you misread their post, that's literally what they said happens. Quote: "the non-exceptional case run significantly faster, at the cost of making exceptional code marginally slower" - they were referring to this as a positive aspect of exceptions as opposed to errors as values.
But why recurse?
@@maimee1 In languages with TRO (and especially if they support all TCO), it is a highly performant and simple way to solve _lots_ of problems. Not so common outside of lisps and other functional languages where good TCO is often lacking.
This article would have been less upsetting if it was called "Why exceptions aren't as bad as you think"
but it also comes from a perspective of a junior-ish dev with little to no knowledge about c++ exceptions. If it was any other language, he could have tried to make a point,. However what he has created shows his lack of understanding of basic concepts of c++, let alone nuances of optimization, or multi threaded impact on exceptions
@@Stasenko58this!
"I use exceptions - and that's a GOOD thing. Here's why"
@@Stasenko58If you mean Primeagen when you say "he", I agree with you. Primeagen is too ignorant about C++ to read this article. He doesn't even know what destructors do. His experience with exceptions is limited to GC languages such as JS and Java. He makes a complete mockery of himself.
@@sqlexp What people do not realize is that exception handling goes all the way down to the hardware even below the kernel levels of abstraction. Every modern processor within their ISA - ABI has a built-in exception handling mechanism built right into specific and designated hardware - registers, and control logic.
From a Java perspective, my impression is that Prime is more against unchecked exceptions than checked exceptions.
Exactly my thought. In addition to this checked exceptions can be passed on to the callers and the final handler will have available to it the full stack trace, however when you pass on error values to the caller information about the failure site (stack trace) is lost.
@@slr150 I don't know why he uses JS as example of Try Catch and not Java, throws in method signature is a thing
The thing is, people complain about errors as values because of the "if err != nil" boiler plate, but if you check all of your exceptions the same way, you get massive try catch boilerplate
@@slr150 That's actually the feature of exceptions I like the most. Being able to trace the ENTIRE error chain.
The point is Java devs have been extremely inconsiderate in the last couple of decades when using exceptions (skill issue), and have created a monster out of them.
@@awesomedavid2012 I'm tired of people complaining on boilerplate code, that is there for a reason, you want control? You have to write more code, that's just how it works, Java gives you that type of control into an higher level envirorment and everyone hates it for that, they should learn how to categorize stuff and put them into the right place and boilerplate will be less a problem
Don’t say you’re “handling the error” if all you’re doing is checking if an error exists, add a log msg, wrapping the error, and then returning that error up the chain. That’s literally what exception throwing is doing for you for free.
I suspect Prime does not have a ton of experience or proper understanding of exception handling, many devs do not fully understand how to implement it properly in large applications.
@@depafrom5277 Suspect? Doubt Prime has any notion of low-level functionality, his videos are so horrendous that I end up going and reading the article myself. Nothing of value is gained by sitting tight and swallowing the whole video.
I recently learnt that some "high-skill-low-level-system-c-dev" doesn't know python is strongly and dynamically typed. I thought they would know at least THAT before saying "python bad". Many programmers, even(or especially) skilled ones, don't know basic things "within the profession", outside their own specialization.
@@depafrom5277 He started out with Java, so he should be experienced with exception handling although checked exceptions are the devil. Even C#'s exceptions still have a lot of rough edges when you try to use them as error handling. Then again, Prime has been on an assert arc which I don't get. I can understand not parameter checking because you're lazy or time constraints...but discovering that parameter checking (in code deeper than user input) is a good thing should not be happening at the senior dev level. Maybe someday value objects will pass his radar.
It's very different. Explicit acknowledgement of errors where they can happen is essential to maintaining software in production.
That said, if your app can function comfortably letting errors bubble almost all the way up in the long term, I'm happy for you.
My approach: exceptions are for exceptional cases, result type with errors - for expected failures like user input validation, not found, unauthorized, etc.
This needs a boost. All errors are not created equal. There are categories: avoidable vs. unavoidable, or recoverable vs. unrecoverable. I've seen errors used in place of predictable branches.
So, the premise of the article and the response is an overgeneralization or lack of clarity of what _error_ even means.
This isn't just your approach. This is exactly how it was supposed to be.. I feel that Prime really missed the mark on this one and the author made extremely valid points for exceptional cases. He didn't argue error values shouldn't exist, he argued that they shouldn't replace exceptions or be used where exceptional cases happen.
The comments and Prime both seem to have missed the key point in the handling section:
Thrown exceptions _force_ the error to include context by creating a stack trace etc. By having the language handle exceptions, you can enforce certain rules about your errors that you can't when errors are merely values and then you are at the mercy of the individual writer.
1. The stacktrace is not always necessary, literally, when you describe your errors as strong types modeled in your application. LITERALLY, they are not needed. And MANY times the stack traces are devoid of any meaning (for example, when you are dealing with an event loop).
2. This is not something exclusive for exceptions, you can pretty much have stack traces in errors as values as a default for a language (and you can do it in many languages with libs), so it do not make to say this is a point in defense of exceptions.
I can maybe kind-of buy this one, it's the best argument I've read so far, but it also feels contrived. You need stack traces only when a human needs to look at the error. If the lib you're using returns crappy errors (without proper context) then yeah, you're somewhat out of luck. But... if it throws crappy exceptions to stack frames that mean nothing to you, you're likely just as out of luck. Aka the lib author wrote bad error handling. If it's your own code, then you can trivially get traces on every single result with `anyhow` Result type.
I don't understand. What sort of rules can you "enforce" that you couldn't with e.g. Results?
Not disputing the enforcement point, but I never get any value from 150+ deep stack traces when debugging service handler errors. Then in production, Google Logs even truncates the log so you can't even see more than the top ~30 frames. Custom human written error messages are much more useful for a human to understand.
When developing a program you want to know all the ways a piece of code can fail, so that you can make sure to handle those ways in the correct way in the future.
Even if you have the stacktrace, if you don't know how to handle a specific error having the entire stacktrace won't help you. An error as value solution would then just crash and provide the stacktrace to en engineer that could analyze it and figure out how to handle it in future versions of the code. All of this should be done before sending it to production, otherwise it isn't a robust program we are talking about and in that case we are discussing something else.
Errors as values allow you provide the correct amount of information to handle the error in those future situations, so that the program can go on as it normally does.
If you do this using checked exceptions instead, fine by me - the same in my book. I would just question why this new exception thing requires new language constructs and syntax rather than just use the same type system, function call convention and return value handling as everything else in the language. Feels unnecessarily complex, but I don't really have that much against it.
I prefer languages where I know what errors the function can have. If a function is updated with a different Exception, then the caller can't know this. Program will happily run, even if the Exception is not handled. But if the error type was coded into the function signature, then the caller only needs to know about this function and not the entire stack. And any change would make it incompatible, and force the caller to update to handle any error.
That's none of them not even error languages
@@thewhitefalcon8539 What do you mean?
you seem to be describing checked exceptions in Java
And if the function acquires a new error code? Also, if an exception is not handled you'll get a termination (unless you have catch (...) somewhere).
@@ScorgRus I look at this from Python vs Rust error handling. If there is a new error code, then at least in Rust it has to be handled. Unlike if there would be a new exception throw in a library or function, then you would not know until it breaks.
But if the new error type is part of the function signature, then it means this code cannot compile and run, because its incompatble. That means it won't bite you at runtime. And you know for a fact what possible errors can be returned, therefore there is no need of any function stack knowledge like with Exceptions.
In example you have a GUI. One of the functions will activate a function if you press the button. That button may load a file from the web, open it, look for certain patterns and display the resulting text for whatever reason. There are lot of parts that can fail. With exceptions you have to handle every possible thing that could fail. Because that function may call other functions that can throw, you don't know. On the other side you have error types that return very specific errors. You are 100% sure that all other errors are handled and only need to handle these returned errors.
Then later someone adds a new feature and new possible error throws in 2 different functions that the GUI is using indirectly (2 levels deep in the call stack). How do you know this?
Error handling in complex systems is highly contextual. You might not have use a single silver bullet that kills all problems at hand. For example when you write SC software with high performance requirements you tend to prefer return error by value, while errors in a UI exceptions are usually preferred.
Yes, ironically because if your UI code throws, there's probably not a good way to handle it, and you just want to tell the user that the program isn't going to be able to do what they want it to do.
the main trick to recover from OOM is to always have the memory pre allocated for handling the error, and/or(usually and) having some of your used memory being forced reclaimable, like if it was minecraft you could have pre-allocated memory for everything u need to drop chunks (sections of game world) from memory.
Minecraft used to have a useless 2 megabyte array. On OOM, it would set the array reference to null before showing the fatal error screen
most of the time whenever Minecraft runs out of memory it just crashes, it only recovers from out of memory in very specific locations
Easier said than done!
Bro has never used rust web frameworks. They all return 500 code when a handler panics. Exactly as a top-level exception handler would. Effectively, unwrap is throw for most web frameworks in rust.
and its not realy hard to implement when you need it...
So what you're saying is they have shitty exception handling ?
@@getattrs no, they have the normal rust Result based handling, but if a handler calls unwrap() and panics, then there is a default handler for all panics that does a sensible thing and keeps the server itself running. Of course, you can customize it as you see fit, same as in C++. When people say rust does not have exceptions, they just do not know how panic and unwind works and thus assume they are not supported. And in reality they are just named different and tucked away so devs do not abuse them for everything as in c++.
@@getattrs That's what literally every single web service framework already does lol. Spring Boot 4xx/5xx status codes are literally just exceptions
@@alexpyattaev 1. Rust panics absolutely are shitty C++ exceptions. 2. C++ devs don't "abuse" exceptions, they _use_ them, as they should because it's the superior error handling strategy for most situations. 3. saying panic is "supported" is a silly argument in favor of the rust model because rust developers will generally design their code around the errors as values models and therefore drop a bunch of spaghetti at the first sight of a panic. Having exceptions theoretically supported and having them as well-integrated, idiomatic parts of the language are vastly different things.
The argument about Java's OutOfMemoryError was completely wrong because Errors in Java are not even meant to be caught at all (google the difference between Errors and Exceptions in Java).
Exactly. Guy honestly doesn’t know wtf he is talking about.
Error is just a subclass of Throwable and handling errors and throwables (not exceptions) can make sense in some systems in some cases. You can trigger log procedures or native calls or close any db connections etc.. and afterwards throwing those errors (throwables)
An out of memory error only means that no further memory is available for your process, however the JVM still runs as the memory was already allocated, only if an error (here oom) is thrown the JVM then logs and crashes, then the memory is freed... ignoring fatal errors is bad.. but saying that they should never be handled (caught) is also false
@@KakyouKuzuki2001 You can just do try-finally so the error can propagate while you still can clean up your resources as needed.
This whole conversation seems a bit heated and does not get down to the core. Exceptions and error values are the same thing IF you are talking about checked exceptions. Unchecked ones are what Prime has trauma with. I am working with some very performance sensitive code and I would NEVER use errors as values as it would literally half the performance. I think it is very reasonable to have a separate unhappy path when the unhappy case is a rare valid state or a coding error.
It is the opposite for me, my C++ program performs better with error as values even with the Zero cost exception ABI that C++ uses. I never measured it though, it just an observation, maybe I would document it next time
@@cyrilemeka6987 That's the thing with performance, you never trust it until you measure ;)
@@lapissea1190 yh true. I am actually designing a language and one of the hardest semantics I had to define bothers on error handling. I ultimately settled on error as values but with as much syntactic sugar(similar to zig's error handling) as possible.
@@cyrilemeka6987 Yeah that's what I notice. If you have more complicated errors, then they are better as values
We also have performance sensitive codes and we also benchmarked it to show the obvious: Exceptions perform far better than error codes. However, an executive decision was made and now we use error codes. After all, a gut feeling is worth far more than actual benchmark data. ;)
Unwinding is not for printing the stack, it's for calling object destructors to release ressources, like unlocking, closing system handles, freeing the heap etc. All of that automatically!
In rust you can recover from a panic. Most server libraries do this on every connection.
Same in go
If I'm correct, "std::expected" is slow because the copy constructor of the "std::string" is being invoked every time you construct the "std::unexpected". To reduce memory allocation, the author should've used the move constructor by using "std::move". Therefore, the latter implementation allocates string on the heap multiple times compared to the exception implementation.
Maybe the compiler is unable to optimise the allocation or the pattern because the "std::expected" is a new feature, but I think it'll improve in the future as more use cases are optimised.
Yeah this code is really weird. I don't know why they reconstruct the unexpected value, when they could just return the local variable so it's move constructed. I actually misread the code at first because I have no idea why anyone would do it like this...
Also, c++23 now has support for monadic operations to std::optional, so a more fair comparison would have used that instead.
@@geek2145 I just looked into it quickly inside Godbolt to confirm what I said, and I'm not saying something wrong. I saw calls to the "new" function that shows it's calling to the copy constructor. I used clang c++23 with O3 flag.
@@geek2145 optional is not an error, nor does it contain error context (edit: but I totally agree that there is no reason to make copies) (edit 2: actually unless you can get copy elision going here, the string will always be copied due to SSO, which is a stack operation, but still not free)
@@geek2145 The article was talking about exception allows you to pass more context to the catcher using strings. So, I doubt he'd use optional since it only allows two states; either you have the value or not. It doesn't allow you to return any other value or context using value.
Why not both? Errors as values are great if you foresaw that particular error happening; know what to do about it; and are actually able to at that point - "the unhappy path". But you can't always do that. You want that operation to crash, and have some higher level thing in the system put the system back into a known good state. This is what Erlang/Elixir do, and the approach is a big part of writing resilient systems on the BEAM VM.
I like the mixed approach, my problem is more when the language leads you into dealing with your errors as exceptions first instead of representing them as simple errors (like JS, Java or C#).
You can't have both. One or the other will became idiomatic, every library will use it and you'll have to use it to. Or you can get c++ situation when there is nothing working together because each library use different string, different threads, different exceptions etc.
@@Zuftware
You can absolutely have both if your language is structured for it, exceptions were made to be , people made them the default in languages that have them because options were not provided for the other case.
Both is the way! Unhappy path has a lot of "flavours"; it's rather a spectrum really. Assertions/sanity checks that the "state of the world" in the program still realistic - yes please, blow up an exception, there's nothing to do anymore. Anything else - it depends, it's "on the spectrum"; most likely, error values is the way to go.
Joe Armstrong was low-key right about everything. Erlang does train you to expect exceptions or things to blow up absolutely anywhere.
Recently at my job I was parsing a binary file containing data structured to a given schema and encoded to binary with a known algorithm. The program was written in Ruby. When I was done with the logic it took me less than 10 minutes to wrap my key-value parsing loop in a try-catch block, catching any exception and skipping the offending k-v pair (while informing the user of it). The result was that all of my parsing code was written naively and any null pointers, array out of bounds, division by 0 etc. as a result of invalid input was handled without me having to think through all of the failure modes and all of the places it could go wrong. Even with a type system to inform me of those places, it would have taken hours if not days to do what took me 10 minutes. I work in embedded and most days I write C 8 hours but I was very glad I didn't have to do that specific job in C because it was for a host tool. I think being able to handle such errors through exceptions is valuable and valid.
This. Exceptions allow you to only care about the happy path. Sometimes, you really just want a goto statement to jump higher up the call stack where you can do something useful about the error. Needing to check errors on every function before using the result is nothing but worthless boilerplate when you already know you can't do anything about that error and you have to pass that up the call stack.
Yes, unwrap exists, but in that use case, it's just worse than using exceptions. Unwrapping errors and providing type information on those errors means you're re-implementing one of the most annoying parts of async programming because you now have blue/green functions again, where you can't call a "green" function that returns an error without turning your "blue" non-error function into a green function. It's the worst parts of Java checked exceptions for an entirely new generation.
I don’t mind Swift’s model. It’s basically unchecked exceptions except a function needs to be marked “throws” if it can error. All calls to throwing functions need to be called with some form of “try” before the function call (similar to await). You can “try” which will rethrow the thrown error, you can “try?” to convert the result to nil, or you can “try!” to crash the application. If you wish to handle an error then just surround it in a do/catch block. You can test whatever errors types you want to explicitly handle.
A function can also be marked “rethrows” which says that it only throws if a callback argument throws.
They did recently add typed throws, but you can only declare exactly one error type, and its use is discouraged for most applications.
@@MessioticRambles This looks very much like colored functions once again, just like @alexlowe2054 wrote above.
I fail to see how this would've taken me over 5 minutes in Rust. In what way is a match statement's default branch not sufficient? I do this every time I want to find all the errors in a pile of unprocessed data.
@alexlowe2054 Prime made fun of that guy for the same thing you just said, unironically. Using unwrap is peak sloppy code. He didn't explain why, so I will - everything that can fail can go in a match statement, and the default branch can be used to do whatever you want with the error message as well as handle the error however you want (including logging it and ignoring it like in the example from OP).
I'm starting to think nobody understands both exceptions and errors as value because I don't understand exceptions at all, and I was 120% with Prime, yet in the comments half of the people are making points that are surreally ignorant - like saying you can't do something that is the bread and butter of a language which in this case is using the default branch of a match statement in the same way you all are talking about using noncrashing exceptions.
I think he stumbled on a lesser-known landmine like the spaces vs tabs Holy war.
C++ guidelines:
*I.10: Use exceptions to signal a failure to perform a required task*
*Reason* : It should not be possible to ignore an error because that could leave the system or a computation in an undefined (or unexpected) state. This is a major source of errors.
(...)
*Note* : We don’t consider “performance” a valid reason not to use exceptions.
* Often, explicit error checking and handling consume as much time and space as exception handling.
* Often, cleaner code yields better performance with exceptions (simplifying the tracing of paths through the program and their optimization).
* A good rule for performance critical code is to move checking outside the critical part of the code.
* In the longer term, more regular code gets better optimized.
* Always carefully measure before making performance claims.
+billion
Yet, C++ is implementing errors as values. I don't think they are much convinced with that.
C++ the alpha male language.
Whoever came up with the idea to display your keystrokes is a genius. Literally learning vim by osmosis thank you
At its core there are very few differences between errors as values and exceptions, it's really just a slight syntax difference, and most of the problems people have with one or the other comes more down to implementation details of the language.
For example I could write a wrapper function in most exception-based languages that runs another function and returns a tuple containing either the return value or the thrown exception. I could do a similar thing in for example Go to turn any returned error into a panic. The only real difference is whether you like your default error handling to be with a try/catch or with en error check.
Personally I'd like a language to support both. I really like the try/catch pattern for aggregating error handling across multiple lines, but it's absolutely awful when it's just wrapping a single line of code. I like the ability to return a value AND an error like in Go, and I like the ability to ignore that error if I'm confident that my input can't cause an error, and I also like that there is a distinction between expected and unexpected errors. What I don't like in Go is how function authors can't mark errors as "probably a bad idea to ignore" because they can be caused by side effects even with proper input. I also prefer the clarity of a typed throws declaration which tells you at a glance if and how a function may error.
I'm 10 minutes in and already experiencing the pain I was expecting. I know exceptions are contentious and I understand why many people prefer errors as values. But Primes critique of exceptions, namely "if you want good error handling (with exceptions), you'll litter your code with try-catch", is just a skill issue. If you find your code littered with try-catch, you're doing exceptions wrong.
The webserver example given by the author is perfect for this. There will be a function that handles one single request from start to end. That function will have a try-catch wrapping receive->process->respond. If an exception occurs, we can log the reason and try to send an error response. This covers 99% of your error cases perfectly. You might find some specific error cases that should be handled in particular ways, but those are the vast minority.
Yup, the author made several great points that for some reason Prime didn't understand despite them being somewhat clear. I think he was just too defensive on his stance and basically at one point he was literally making an argument for exceptions in his drawings without even realizing.
About 13:25, you follow the RAII style. Don't ever manage resources manually. Wrap them in an object that releases the resource in the destructor, like unique_ptr. That knowledge is implicit because the writter is probably a C++-programmer that is so much used to think in terms of RAII that he forgot that using dynamic memory manually was even a possibility.
Imagine calling a raw new instead of std::make_unique. I could never.
I don't like the unwind argument that much, because a `return` _also_ unwinds the stack, in both Rust and C++. `throw` _does_ unwind the stack more efficiently than `return` does, though, which I think is the argument that the author is _trying_ to make.
In C++ I use a template class called defer_action which accepts a lambda - write a cleanup lambda to clean up any arbitrary resource (or back-out actions like “unregister”) and then supply to the constructor of this class. It will do RAII for that arbitrary resource.
It also has a disable method so the action can be avoided on function exiting, and a method to explicitly invoke the action and then put it into disabled state. These two methods are occasionally useful for when the cleanup will no longer be necessary, or if for some reason there’s a point in logic flow where need to explicitly invoke the cleanup (instead of allowing to be deferred to outer function exiting time).
This template class is hyper useful, gets utilized all the time, and it’s very puzzling to fathom why the C++ std template library doesn’t have equivalent of such. It should have been there since lambdas were added in C++11, but yet it doesn’t exist. However is super easy template class to write. Yet because RAII is a centerpiece of C++ goodness there should be standardized practice around such a facility so that new C++ developers know to use it and how to use it.
Even decent books on learning modern C++ don’t bother to illustrate such a template class and educate readers to such - is weird that these authors miss out on aiding newbies with something so generally useful and very often needed. What programmer doesn’t deal with resources other than memory allocations? And it’s extremely tedious to write a custom RAII class for every occasion this comes up. And it’s kind of lousy to have to hack std::unique via custom delete functions to deal with these cases. The smart pointer template classes were aimed squarely at RAII management of memory objects.
Guess should try my hand at writing a proposal of adding this to the standard as no one else seems to have done so (am I wrong about that?)
@@TheSulrossIt sounds like you're describing a scope guard. lots of similar implementations exist. and I agree, something like a generic resource management object, (even a "before/after" paradigm type of class) would be useful in the standard library
@@yyny0
" `throw` does unwind the stack more efficiently than `return` does"
Why exactly?
I am always so happy when a Starcraft reference is dropped. APM driven development, let's go!
coming by this a month after its uploaded, i randomly feel so seen rn
“How do you know the above state doesn’t get effed? Did the above state expect if to be thrown?” - you can easily make it very predictable even with unchecked exceptions. For instance, define a PermissionDeniedException, and make it a contract that this exception is thrown if and only if you server api should return 403 response. And that’s it, you never need to catch it in your business logic code, just throw it at any point where it’s convenient. You won’t need to write hundreds or of lines of code throughout your whole codebase just to bubble this error or pollute your function signatures with the concern that your code can’t and shouldn’t do anything about
There is a time to use exceptions and a time to pass back errors. Neither time is 'never' nor 'always'.
Ok so where am I wrong here? Go for example forces the checks of error values basically everywhere. How is this different than try-catching EVERYWHERE? If you write the checks for exceptions as often as Go forces you to check for error values aren't you essentially in the exact same situation? You can handle the exceptions in the exact same way?? Right? So really Prime seems to just like that languages with errors as values FORCE the check throughout the code where with exceptions you have the choice of sending them up the call stack by not checking them?? Am I lost?
No you're right. On a higher level that is exactly what is happening, there's not much of an abstract difference.
And it completely rubbishes the performance argument. By his own admission, handling errors are an order of magnitude slower than checking for errors, so if a large part of what your application is doing is validating inputs to decide whether to act on them or not (which seems like 90% of what you do in a web application), you will be throwing errors a lot.
@@Musikur input forms are a bit of a special case. because you normally want to collect all errors to show to the user, instead of stopping after the first field. this typically extends to the back-end, so that it can be used in an input form (if you have a separate front-end and back-end). i'm not disagreeing with you, as i could see this work with Exceptions or Error Values. this is just a tangent
C++ makes exception handling quite straightforward. Do you have any state that will not be cleaned up by stack deallocation? Do you call any functions _not_ marked noexcept? Use try/catch. The reason there are so few try/catch in that database example is because the answer to the first question is almost always "no". This is because C++ heavily uses RAII, where you either get an exception while trying to allocate resources (and the resource itself will clean itself up before throwing), or you obtain the resources in a state where stack deallocation will automatically clean them up. This means exception handling mostly needs to handle rollbacks of things that need to be atomic but aren't naturally atomic.
Prime is talking in context of his experience, which is not the most common. I usually use a mix of the two because when writing a HTTP APIs. Exceptions are fine in my case because 1. the services are stateless, and 2. there are a large number of cases where I want to immediately terminate the request. Exceptions allow you to very quickly achieve the desired behavior of returning 404, 409, 400, etc. without any care for depth of call stack. Result types are nice when you want to pass the termination decision to another class. Personally I like error result at the infrastructure layer so that my application code makes that decision, it keeps everything in one place.
Ah a web dev talking about "experience". Classic.
@@stysner4580 I dont only do web but it is my experience in professional env with massive systems. I cant make statements in other areas such as infrastructure, tools and games because my experience there is based on small applications. I presume you dont have exp in large server side web apps, otherwise you wouldnt make that comment. Thats fine but my point is that tools dev is a small industry compared to web. So giving advice from that narrow experience band as if it applies everywhere is incorrect for most devs. Such narrow views is also bad for new devs coming into the industry (It seems that most of his viewers are students, juniors and academics).
> Personally I like error result at the infrastructure layer so that my application code makes that decision, it keeps everything in one place.
I started doing the same in recent year, especially for calling HTTP API. I'll have a try-catch in an infrastructure method to capture the exception then return a result class back to application layer to let it decide what to do. Infrastructure concerns and application concerns are now completely separated.
@@abcabc-ur3bf 100%, I still like to use exceptions inside the application/domain because it is responsible for knowing what needs to happen in a particular case... If you pass it back up to presentation layer then it needs to make that decision, which I dont like because I view that purely as a translation layer.
@@brandonpearman9218 Then don't? Just transform the error at the application layer into a user error for the presentation error to display? It's no different than throwing an exception, you just write it in the function instead of in a wrapper around the function. I'm not sure why people think that EAV means you MUST pass the exact error the whole way to the top and then back to the client. How many times do you just stick the message and stack trace from an exception into the return message to a client? Never hopefully.
Early bait: FP is the best paradigm because in order to get a fully initialized type, you must handle any errors along the way.
Not really:
var nullOrValue = Try.of(() -> "foo")
.mapTry(this::someExceptionThrowingMethod)
.mapTry(this::randomNPEMethod)
.getOrElseGet(null);
what is a fully initialized type?
@@Daniel_Zhu_a6fYes I'm new to FP. I think it's referring to immutability, like, if you're building out a data structure or value via multiple functions, if one fails, your code fails. Whereas with objects which have multiple methods to get them to the state you want, it can be more difficult to trace out and maintain "good" state? But I feel like both paradigms have ways to solve that, so I'm probably wrong. I figure if I post the wrong answer someone will come around and correct me. 😅
@@Daniel_Zhu_a6f a good example is a file handle. With a Maybe monad (FP) you must handle the file not opening. In procedural (fopen) you usually have to remember a null check. OO approaches usually throw an exception in my experience.
Haskell has exceptions in their "spec"
Ahh... error handling, the bane of our existence.. should you return, throw, exit, pause the stack, report to the user, log the error, etc.? How much context to include? stack trace? specific variable states? And how much is all of that allowed to affect performance?
Only exit if the program depends on a certain value to be within some parameters or some operation having ran. In all other cases it depends on if a default value is acceptable. What's left then is deciding if the error needs to be logged.
Recursive fibonacci is probably the first function most people use when learning how to write tail-call-optimized code. It doesn't even require full TCO support in the language, as it only needs tail-recursion-optimization, which is much easier for a compiler to implement. His version doesn't use TRO, as the last thing before the return is an add. The TRO version uses an accumulator value passed as the last parameter to the function. The recursive function call is fib(n-1, acc+n), which is fully TRO (and by extension TCO) friendly.
At the outset I’m struggling to see a real world difference if you’re managing exceptions properly… try catching everything is horrendous but if I had to do a check on every returned value is it much different?
The way the error is handled, and with exception, we sometimes don't know which code throws and which code don't.
not every value, only error values. ie a result type in rust or error values in golang etc
Yeah, he complains that the author misuses error values and then misuses exceptions.
@@NootlinkHave you ever used Java?
You basically replace catches with ifs. I guess "errors as values" enjoyers find it better because the "try" block causes nesting and visually could get ugly.
Do COM component programming, and get both paradigms in the same codebase. Intra-component C÷÷ exceptions leading to returning HRESULTS at the interfaces between components.
I've worked with exceptions and return values for about 30 years, and I don't think either of them has a major advantage. Checking fault states at every step must, by virtue of syntax, be a horrible, repetitive nightmare. Job security, right? The business logic and intention of the code is obscured by error handling... oh, well. The truth is that in practice, the better way is the one people have the most experience with. They both work, and they both make use of convention to plug the holes in their design.
Hey Prime, I really appreciated this conversation around error handling. Thank you!
Exceptions are the best way to handle error when developing stateless backend applications.
All you have to do when an unexpected thing happens is: throw an error, rollback database transaction, report the error to monitoring systems and return 500 response with some sorry message.
You can easily implement this kind of universal error handling mechanism with exceptions or try-catch.
can do the same without exceptions. Plus exceptions are way more costly in terms of resources.
@@jordixboyYou can't do the same - the result might be the same, but much more code is required. The point is that with exceptions, you only need to consider errors in *one* location.
Resource usage is not much of a concern, as exceptions are used for exceptional situations that do not occur often.
@@vinterskugge907 most apps nowadays use exceptions as default error handling, so exceptions everywhere - they are not treated as exceptions
@@vinterskugge907
"much more code is required" and?
Your point is seriously that all error handling should be made to the point of one single catch location?
@@diadetediotedio6918 Remember from the original post: "when developing stateless backend applications".
I am not saying that *all* error handling should be like this. Rather that, for some types of systems, exceptions are superior to error types because you never need to think about errors except in *one* specific section of the code.
The same applies to batch jobs, which I also happen to work with.
Python's 'context managers' deal really well with localizing errors. You couple local error handling with the resource in your resource's ___exit___ method. In there you receive errors as objects and handle them as you please keeping all your resouce-related ifs in the same place. Basically, Python has RAII
Exceptions have one big advantage: automatically including the context (stacktrace). Errors as values mean you have to do it yourself, otherwise you just get something like: „file XYZ doesn’t exist“. Also just having exception handling at the top level and the rest of the code being the happy path is nice if all you can do is fail most of the time.
And that's also why i don't like exception. I want to control when and where to put stacktrace, it adds unnecessary bloat and slowdown. You know how slow (in Python) catching KeyError is instead of testing if key is in dictionary?
@@hanifarroisimukhlis5989 don’t bring python as an argument. It’s a terrible language with a terrible interpreter.
Sure, errors give more control, but also requires active work to get the same infos and discipline to get the same behavior everywhere. The Python thing is another point, the language itself is slow and this case could be solved with a better API. If there are cases that are highly likely to fail errors or in this case a bool if it exists (multiple return values) are preferable. If my web app can’t connect to the database or the result set doesn’t contain a required column then the request fails. No need to repeat error checking for every tiny operation that fails. Nothing the code can recover from. The price you pay is performance in the case of an exception. Totally fine it that is actually the exception and not the norm.
Well one can think of a ? operator that attaches context automatically.
That assumes you keep bubling the error up instead of handling right then. If you're calling the function, you know the context, then depending on the situation you decide wether you handle the error right then and there or if you need the caller to decide, if its the latter, usually you'd repackage the error into something that makes sense to the caller of your function.
In rust that measn I just do a bunch of map_err(...)?, its pretty straight forward and quick , doesn't bloat or obscure anything, it just flows from one result to another smoothly.
The Tiger style article is soooo great! I had never heard of it. Thanks Prime
Exceptions would be storage device/peripheral failures, solar flares, comets hitting the planet, or crossing the Bermuda Triangle/any place with high strangeness such as haunted mansions orSkinwalker Ranch. The last thing you'd expect to be an "exception" would be file read/malloc errors as they're just things you need to anticipate or else you're not a "real software engineer".
@@SimGunther This is a common misconception. The word exception is a poor choice, because there are no exceptional or unexpected situations in programming. It should have been called “Error” instead, because that’s what it is.
So this is kind of the opposite of python's mindset of using exceptions for controlling flow? I'm surprised I don't see a lot of python devs jumping in to these errors as values videos
@@youtubeenjoyer1743if the circumstances aren't exceptional, then why do they justify being thrown? Why not just return a value? Some functions DO just return a value, like a -1 returned by indexOf when there is no index. That isn't exceptional so it doesn't warrant interrupting control flow
@@awesomedavid2012 what do you mean by “interrupting control flow”? Error handling looks pretty much the same in complex applications, it’s a pattern, if you will. This language feature attempts to make error handling more convenient.
@@awesomedavid2012 Sentinel values only work in some circumstances and are generally just awful to work with, since they require you to check what sentinel value a specific function uses. It's something you learn over time for sure, but if working with a brand new library, what then? Also, assume -1 is valid, what do you return instead?
Another scenario, assume something changes in the library and the previous sentinel value ends up becoming a valid value. Now you've got to track down every single case where that's a possibility and change it. Obviously that's the case if you throw a new exception as well, but you'd hopefully be notified with errors in your IDE or compiler if that's the case.
So tooling is a major benefit when it comes to exceptions compared to sentinel values and there are better solutions to sentinel values regardless, such as the Option monad. Option either having Some or None is far superior to a sentinel value as it immediately tells you what you're working with in the type definitions. Because as you say, it's not exactly an exception or error that the value you're looking for is not present in the array/collection, so semantically it makes sense to not throw in such a case. I think it makes more sense to change the return type rather than using a sentinel value however.
Another alternative is to have a more complex object. Example:
Collection.find(E item)
Returns contextual information such as isPresent(), indexOf(), replace(E item), remove() and similar. That way you're dealing with something you can directly interact with in a more natural way (from a non-programmer perspective) and also do everything with built-in functionality.
The only argument I have in favor of sentinel values is performance in low-level code. Integrated systems and whatever else. That way it's easier to reuse memory and ensure you're not blowing up the RAM usage for no good reason. In any other situation, I think just having a different return type is more reasonable.
I prefer Swift error handling… It basically uses the syntax of exceptions but the behavior of errors. You put a throws at the end of your function declaration to say that it throws an error, this require you to put a try in front of a call to a function that throws, if you want to handle the error you put the try into a do { } catch { } block with catch being able to do pattern matching, if you don't want to handle it you just say your own function throws, but you still need the try keyword in front of the call… And you can also use try? where if an error is thrown you get a nil return value instead, or try! which will crash your program if you get an error. And yes, the resources are properly handled, you can use the defer { } statement to ensure that even if you return early some code is executed.
I love it because it keeps things explicit and force you to handle each level properly, but it doesn't clutter your return value with types that can be two different things. You have one path for errors and one path for working code.
I'd be interested in a deeper discussion on Zig's decision to forgo error structures. The article's point seems to be that error context is super important, and Prime's rebuttal is that exceptions aren't a prerequisite for error context, but it seems that Zig explicitly abandons error context and are proud of that decision.
I'm sure I'm missing something fundamental, but I don't know what it is.
The first thing you should ask is: What does zig have to do with prime's arguments? Is Zig the unique language that deals with errors without exceptions?
This is why the question mark operator in rust is so amazing. It makes errors as propagating an error as easy as not catching an exception.
Regarding performance, we have the following dichotomy: absence of error and presence of error.
- Exceptions are fast in the absence of error and slow in the presence of error.
- Error values are medium in the absence of error and medium in the presence of error.
Since the absence of error is expected to be (much) more common than the presence of error, exceptions are expected to have better performance.
If the absence and presence of error are similarly likely, using exceptions is an anti-pattern, which even has a name: "exceptions as control flow". In that case, you should use a return value, even in a language that supports exceptions.
exceptions are fine, as long as you have some way to manage them with the type system, and they cannot be left unhandled on main scope. There should be some type `Except` with functions `throw(err: E) -> Except` and `catch(action: Except, handler: Fn(E))` that can be eliminated with something like `runExcept(action: Except) -> Result`.
Oh wait, that's the `Exception` Monad in Haskell
@@squishy-tomato Unhandled exceptions don't introduce UB, what 'undefined state' are you talking about?
17:00 I absolutely hate that about non-compiled languages (and exceptions, for that matter). You never know an error could happen until it does.
I feel the discussion is about the wrong thing. The real discussion is what errors can and should you handle gracefully? The problem with trying to handle unexpected states of the program is that in many scenarios it will cause more problems than simply bailing out and fail the entire operation and have the programmer fix the bug that caused the unexpected state. The Consonants code is a good example of this, where if for some reason we cannot parse the JSON we return false, which hides a problem that might compound into much larger problems as it might go unnoticed for a while.
This whole discussion comes down to a true statement that if you don't handle errors/exceptions when you should then your program is less robust. Instead of arguing which language syntax allow you to accomplish that better, just write useful tests. Unchecked exceptions have their weaknesses, but they also fit server side programming very well, when we just return 500 when they occur (or other codes for some exception types). In case of resources, at least in Java we have syntax such as try-with-resources or finally.
TLDR; somebody thinks that throwing exceptions is better that `unwrap()`-ing `Error`s, because he doesn't know what else to could do with them. They're not wrong.
They're wrong because if you don't know what to do with errors, you return them upstream. As Prime described, unwrap() is for asserting program correctness (i.e. impossible state) rather than ignoring errors.
@@ivanjermakov that's why I don't like ? -operator, makes it too easy to move the error to someone else
I don't think that's really the author's point, more-so that throwing exceptions is better than re-raising errors. Which it is, re-raising creates branches which as explained in the post can trash performance in a number of ways.
@@defeqel6537 It is someone elses problem, because it's often contextual on the caller.
For example:
function save-my-stuff-to-db(stuff) { }
function write-important-data-to-db(important) { save-my-stuff-to-db(stuff) }
function write-transient-garbage-record-to-db(garbage) { save-my-stuff-to-db(garbage) }
If returns an error value, do you believe that handling it as a value within that function is behaviorally correct given the two different caller contexts?
@@FrancoGasperino it may or may not be someone else's problem, but ? is easy to abuse and lose context
in your example, what if you are saving stuff to 2 different DBs? And you get an error message from one of them, but fail to pass that information to the caller, and instead just ? the error forward which is the same error for both DBs
One (somewhat) nice thing about exceptions is that you can just ignore any exceptions except at the very top level in deeply nested code. If I've written some recursive descent parser in a C# GUI app where uncaught errors are verboten, it's liberating to just ignore 90% of errors (other than null deref) in the code for the recursive descent parser and only include try/catch/finally in the top-level user-facing code for something like a form. As others have noted in the comments, C# handles unwinding the stack, deallocation of memory, etc. for free.
OTOH, one nice thing about error-returning (or even the super-old C paradigm of setting an errno) is that it's a convenient paradigm for some process like a JSON parser that is progressively constructing something when a syntax error halted parsing, and you want to be able to show the user the (incomplete) result that had been produced before interruption by the error. You can't really use exceptions if you want to do this, unless you bind the state being constructed to the object that's constructing it.
To achieve exception-safety, the easiest way, theoretically, is to call all functions that could potentially throw an exception first, and then you modify your state after accumulating all the results. That way you don't need to write try-catchs all the way down to revert the state at every possible failure point because you didn't modify any state in the first place. That approach can't be applied in all kind of contexts, but can be done in multitude of them, removing the try-catch hell in the 90% of places. Unless the context of your problem makes that approach unfeasable, the exception-style becomes more readable in general.
agree, handling a state with the exception is not really a big issue as he does claim.
that's assuming you know which functions throw
Regardless of which manner of error (or exception) handling, gathering up new state and then after successfully doing that, mutating the “official” state, is a generally good approach. Essentially try to make even complex state changes to be done in an atomic-like (or actual atomic) manner instead of in a piecemeal manner - has benefit of making clean back-out easier to accomplish.
The advice here stands alone apart from the actual error handling approach.
Have a case in current project where there’s a function that produces a context object - there are a lot of steps involved along the way to fully prepare an instance of this object, which any step could plausibly fail. The function prepares an object instance of this state that as a local stack object. At any point of failure and function return, the object’s destructor will do appropriate cleanup. But if get to the end of function with total success, then this stack object is moved into a data structure to where it becomes runtime operative. The move assignment operator is implemented to be noexcept, of course, so this final step will succeed without failure being a possibility.
It’s a pattern that is used in these kind of situations - a handful of times in this particular project.
@@defeqel6537 The assumption one should make is that everything can fail. This is because in reality everything _can_ fail. If you assume that a function in Rust can't fail just because it returns a regular value instead of a Result, you'll fall flat on your face the next time you get a panic. Or when you get a hardware memory corruption. Or when someone pulls the plug from the wall socket. Computing is an unholy mess and pretending things can't fail will inevitably lead you down the primrose path.
The exception model of prepare-and-commit is the only model that can work in this environment. Even if you use errors as values you still have to do it.
@@isodoubIet You have two distinguish between two types of errors, those that corrups the state machine, and those that are recoverable. For example, running out of memory is a corruption of the state machine, not very different from the CPU itself being broken. The metal itself can't obey you. Your required execution context itself is broken. There's nothing you can do about it in general. The errors that have to be interpreted as "corruptions of the state machine", and hence not recoverable, is not worth to even be checked. Of course, the definition of "non-recoverable" is kinda context-sensitive. In an embedded system where your program is the only thing executing you can add lack of memory as a recoverable error and thus handing it. If you are the kernel, same. If you are a user program sharing space with an unknown amount of other programs where the kernel can even kill you all of the sudden if you are consuming too many resources, you don't even check that you run out of memory. You assume that requesting memory will always succeed. The speed at which you can simplify your code when you assume you will always have memory is, incredible. Besides, in my 20ish years programming I have NEVER observed a program of mine getting a null pointer after requesting memory, NEVER.
I add here programming errors as well. I never check that some argument was wrong except in development. Having a bug in a program is in my mind equivalent to the execution context itself being broken, and so I remove all of these checks for production. I don't even let the asserts there, I remove it from the source code. Of course, you have to test the source code as possible during development before removing all the check. The best way to make sure the code is right is by having as less code to read and check as possible by storing as much technical details as possible in library abstractions.
In Python, there are a ton of libraries which are a hundred times easier to use if you catch their exceptions, it's like part of their API.
No word about Common Lisp-like condition system? Ignorance is bliss, I guess.
What is that?
@@mattetis In common lisp you have an option to resume execution of a function from the point where exception was thrown, it has a built in restart system.
Exception, a condition, in CL does not unwind the stack (unless you say so). The state of a program, the stack, the heap would be left where it stumbled upon a condition.
If you are not handling the condition in your code (the lisp term for everything special that can go wrong in the program, not only exception) somewhere, you will fall into the debugger mode, where you can manually execute something, check up the variables, poke at the memory: choose an appropriate restart and continue execution or abort and kill the application.
Until then, application just hangs and waits for your action.
If the problem was very external, by that I mean something very physical. Read "no internet connection", "device not found" and so forth. Then you can just manually RETRY execution in the debugger after you recovered the network in you facility or plugged in the device or did something else.
And everything will work just fine. Smoothly. As if nothing happened: you haven't lost any data and so forth. You never had a need to restart a whole application just because something went wrong and you didn't handle it.
Human organism does not crash entirely because something is slightly broken. There are ways to let your leg recover if you broke it. You do the necessary (go to the clinic, plaster your leg) and continue your life.
In lisp there is a separation between conditioning [situation when the code cannot execute without a context], condition handling [the decision to make] and the code that actually fixes your program upon the condition [in case of condition-x invoke the decision-y]. Or it can be you.
It can be you, the supreme authority, to decide how the program should restart if something failed down the road.
Why would you care?
In theory you can roll your own system of restarts (for example if you are a C programmer that can just do the same with goto, longjmp and setcontext/getcontext to avoid stack unwinding, you can roll your own system of registering handlers, searching the available and even your own repl or gui to handle the restart), but it is too much work for an experimental project and it is not shared between libraries. So...
You probably won't do it. Here you have it for free.
I've subconsciously been following a large part of tiger style for the majority of my programming career. I've always felt it makes more sense to handle errors explicitly at the point of error and assert states that must be correct in order for the program to function. I write in C++ and do everything in my power to avoid dealing with exceptions.
This sounds more like a rant against unchecked exceptions. Checked exceptions force you to deal with the error at the appropriate place and you have full control over where that is.
Yeah but the syntax of try catch is worse than if err != nil. What if you neglect to catch a specific kind of error? Was it intentional?
The point still stands that you don't know something can throw. At least with error as value you can choose whether to let the caller know if they need to handle a potential error, and for the "little corner" argument in the article: that still works. Just handle the error where you are.
@@stysner4580 with checked exceptions in Java the exception type is in the method signature and you're forced to handle it or rethrow it, kinda similar to in rust if you want the value you need to unwrap, though the safe access operator is a very nice convenience in rust.
The problem is unchecked exceptions exist, and worse, people use them as a kludge to avoid changing method signatures or to pass exceptions through lambdas. Arguably part of the problem is that it's even allowed to catch unchecked exceptions.
And you just need to use like one of the 2 unique mainstream languages that adopted that, one of them is not entirely happy with it and the other had its spiritual successor (Kotlin) ditching them entirely. I think checked exceptions solve the problem, but they are much more annoying to deal with.
@@SussyBaka-nx4ge I agree that a "half baked" solution or just providing more and more options isn't going to fix the problem at all. Errors as values immediately forces developers to make a choice, including passing through errors to higher functions to let the caller know that something is fallible, or not if it's not needed.
Something in the function signature letting you know something is fallible is the most important.
My approach has always been to use exceptions only when something is 'allowed' to go wrong and you have little to no control over it at the calling site.
If it shouldn't go wrong and you can prevent it from going wrong at the calling site, then you should try to and use asserts instead.
A mixed approach is correct. Exceptions in C++ aren't meant to be called over and over. In fact they are slow if they are hit. If you know something will be failing relatively often, you should use errors as values instead.
Exceptions are only slow because of limited optimization efforts. Returned errors leverage extensive happy path optimizations, that's why they 'win' in this aspect (ignoring other implications).
And that using statement is just sugar for try, catch, finally.
The main benefit that I feel you get from thrown exceptions is that you are going to have crashing no matter what language you are using. Being able to call a function or do some operation and check if the result would crash the program and then say, "nah, that operation wasn't THAT important, lets just record that it didn't do what I wanted, get whatever info we can, and move on with our lives" OR be like "no, that really needed to happen, we should probably stop what we are doing here and crash." without needing to actually know all the permutations of failure state right from the get-go is pretty nice for getting quite a lot of fault tolerance for very little effort.
Speaking as a filthy casual who barely programs nor uses a _real_ programming language, exceptions are great for just getting things done without having to mind every single failure case across every level, then refactor later. Having exceptions built-in to your scripting language to drop a stack trace is often just good enough. And if I can't catch an exception or am able to lose data due to failures, it's almost always fixable by just rearranging my code to be slightly less stupid.
But obviously, this is not the perspective of someone building any sizable app, real-time video streaming software, nor a robust telecomms system. I'm perfectly fine with my 3-day joke project imploding from an uncaught recursion depth skill issue.
"exceptions are great for just getting things done without having to mind every single failure case across every level", We are speaking about *reliable production* code which shouldn't crash, prime repeated this over and over again throughout the video, i dont get how you didnt pick up on that
@@ckpioo Hence the second paragraph.
In my rust programs, I would use expect for necessary startup variables and ban the use of unwrap. Generally I prefer passing up error values and handling them sensibly as far as possible. But my rust experience is limited to particular types of programs and my usage might change if I get into a different area.
Exceptions let you put one catch at the entry point of your app that says "an error occurred" if anything happens. Boom, done
I prefer the syntax of error values but the semantics of exceptions.
My personal experience with working with servers is that the code is stateless and operates on data that passes through the functions. If things break, winding up the stack doesn't really break things more than error returns. You will back out to the place where you can actually handle the error if you are not doing things wrong. I don't care much about which type of errors I deal with, but I want the errors that I can get when calling a function to be defined so things just don't go pear shaped in ways I didn't even know they could.
But for most of the web crap, doing anything with errors seem a bridge to far and just return a 500 or 400 error, at best.
The discussion is already over at unchecked exceptions and having ever used JS ever. Yay any random function can die any time and I don't know. Hey Java fixes this, oh wait its just "coloured" again. If your language has functions that make me handle the error then its a win, if it doesn't its a fail. Lets clear that bar first yeah? We all lived in this guys world at some point whenever we used JS we already know what its like. Get there however you want just tell me what the hell can throw/error.
The key to using exceptions effectively is to have the following mental model:
Every line of code can throw, unless proven otherwise.
In practice, you'll assume that close to 100% of code can throw an exception. The corollary: you must unwind your state (almost) everywhere. Unwinding the state is the default, not unwinding is the exception.
For me error as value and exceptions are kinda same, I prefer error as value because its better syntax
Agreed, but exceptions have better semantics.
Errors as values are simple to read and simple to write
Stack unwinding is heavy. However, the stack unwind, providing the higher level code is written correctly, will call the __finally block, or catch, and clean up. However, in languages like C# , "using", guarantees cleanup on exceptions as it is in an implicit try, finally, without a catch.
Stack unwinding is slow, but it makes the happy path 0-cost. So exceptions are faster for rare error cases.
About C++ exception don't close file/network handles: I don't think anyone opens a file/network handle using new, even in pre-unique_ptr code. The handle is stack allocated and thus is closed in the exception cleanup. Also if the new-ed value is deleted in a destructor and the constructor of the object ran through[1], then that is also handled in the exception cleanup.
[1]: Exceptions in the middle of the initialization of member variable is a problem in C++. When that happens the destructor isn't called and you can't find out which members are initialized and you leak memory/handles. I think that's a bit of a design mistake of C++. People handle that by using a member initializer list that itself only calls trivial constructors of the member variables that never throw (I guess except if OOM).
^^ this
The prime’s misconception got me heated :|
@@yapet I imagine the thing with "no overhead in happy path" can also something really nice. But you need to be sure that you only throw exceptions if it's really an exceptional case.
Also about the Fibonacci example: He was all about how you don't write it like that etc. That was just code simulating a deep call stack! Deep call stacks do exist. This micro benchmark shows the difference in that particular case. That's valid, IMO.
Having said all that, I do like Rust more than C++. I think it's just important to correctly understand your tools.
[1] If the constructor of a member completed, and then the construction as a whole fails, each such member shall be destructed in reverse order.
So how does that leak exactly? The use of raw/dumb handle types?
There's a different case X::X() : m1(new Y), m2(new Y) {} which can leak because it may do both new calls before calling the first constructor. Though you would usually let the object do its own new to begin with, so that's a non-issue.
@@blenderpanzi bro, you are reading my mind. Just as I was listening to this section, and debating with myself.
I am honestly too tired to start any kind of elaborate debate, but thanks for pointing this out! ❤️
@@mariushusejacobsen3221 Yeah, I might not be remembering it right. It's a long time ago where I learned about that.
to be fair error monads or variables do take up some space on the stack in general
and I think the author does have a good point about the amount of boilerplate needed; yes the fibonacci example might be contrived but it illustrates a case where generalized errors can be handled easier, and also applies to more generalized cases imo.
Both implementation-wise and in terms of writing, pass-by-value is less efficient in the non-exception path. E.g. it seems in C++ the overhead is only incurred when the stack has to be walked and exception tables queried when the exception is actually thrown whereas in pass by value the value needs to be checked either way.
Plus if you have a section that can generate a certain type of error it's easier to just put that in 1 catch than test the result of every call.
_"Why aren't you becoming bigger, that is way too tiny, way too tiny"_
std::expected is just a variant with standardized semantics for what both values mean (T is the expected value, E is the error)
btw it also has transform and error_transform methods in case you want to either do something with the value without unwrapping it or else annotate the error with stack information
Are we acting like the OOM killer isn't a thing? Unless you're writing for kernel or embedded, recovering from OOM is impossible since the kernel OOM killer will just terminate the process
I think your getting OOM as in Out of Memory and the OOM killer as been the same thing. Its totally possible to recover from an OOM at a program level, as you can set the process to have a maximum amount of memory that kicks in, so the OOM killer will never get that far, as the system will still have plenty of memory. Also you can turn of the OOM killer, and is something I do with Arch, rather just have a large swap and monitor resources, randomly killing processes just seems bonkers having the OS reboot is much safer.
@@keithjohnson6510 The author of the article used the term “OOM killer” when referring to “out of memory” errors.
@@robertmichel9904 If you mean by author , your talking about @rosehogenson1398. She mentions the kernel. So it's not the same as "out of memory" errors, that's different. IOW: A process can run out of memory, but the OS could have Gigabytes left. The OOM killer kicks in when the OS is low on memory, and there is pretty much nothing you can do about that, and is the reason I turn it off. Rather have the OS reboot, especially for Service type VM's. eg. It should be possible to recover when using MemoryMax in systemd, or run-times that implement limits on the heap allocater etc. So the comment "Are we acting like the OOM killer isn't a thing?" is totally irrelevant.
Handling OutOfMemory can be resolved by allocating some memory block during the startup of your application and use this memory to do some error recovery/reporting. E.g. you could free that allocated memory and then your code can recover.
I don't quite understand the debate about exceptions vs result types. Aren't they literally isomorphic?
How are "Result method()" and "T method() throws E" anything but exactly equivalent?
Precisely. The only difference is that the function has to return an error instead of throwing it.
Exceptions are more efficient because they don't have to be checked at the call site. In your first example, the compiler HAS to insert machine code to check if the Result was an error. In the second, the compiler could perform all sorts of optimizations.
My perfect programming languages would have CHECKED exceptions, where you are required to specify exceptions in function types, and required to specify if an exception will be handled (`orelse`/`match`) or propagated (`try`/`?`), but without any checks in the happy path.
@@yyny0 I was talking from the developer experience/usability perspective, since that is what this debate seems to be primarily about.
Exceptions are easier to work with if you have not too many points of failure, or you can handle different exceptions in a single place with some generic error handling mechanism, like printing and that's it. Error codes are easier to work with if you need a very specific behaviour for each different point of failure.
@alexsmart2612 Result needs to be handled by the caller, while exceptions can propagate several levels up until their type is caught (or better: catch-ed). So you can build deep nested function call hierarchies, and handle a special type of error up in a central place, relieving all functions down from that burden - _if that is useful to do so_ .
Swift has full support for retuning an error object with whatever you want in it, and even now has typed throws that tell you the type of the error you can expect. Bad exceptions = bad language implementation.
Being forced to handle the errors has a nice physiological benefit, it makes you actively think about errors in your code in a way that a try catch never does
You can write code that happily ignores potential errors. Exceptions forces you to at least acknowledge they exist, even if you then ignore them.
@@chaos.corner Your just objectively wrong. There is literally nothing that forces you to catch exceptions or even acknowledge they exist (in most languages). Errors you have to handle them, if you chose to ignore them that’s on you, but your code has to handle them in some way. Exceptions can be fully ignored.
@@justgame5508 When you forget to catch an exception, your program crashes with a nice error message and stack trace. When you forget to check for an error, your program keeps running, might end up in an invalid state, and when it eventually crashes, you have no idea what causes the state to become invalid.
@@justgame5508 Fair enough. I was thinking of Java which requires you to either catch exceptions or declare that you throw them. I would consider not requiring that a substandard implementation of the idea. (at least for higher level languages. Low level languages are a free-for-all)
i think the main issue is the lack of typed throws. typed throws (explicitly describing the sort of error thrown) allows for pattern matching and graceful handling.
i like what swift does for exceptions (errors are typically an enum)
I prefers errors as values that are exceptions, like in C++ 😂
The automatic cleanup during unwinding thanks to value semantic is totally ok.
I don't understand the debate
The blog post should just have been this. "Exceptions" _should_ be errors that syntactically look like normal return values but use a more efficient calling convention optimized for error propagation (i.e. `try` in Zig/`?` in Rust). Most of the arguments in favor or against exceptions do not address this.
Exceptions are just part of the throw/catch paradigm which solves the need in the most used case of goto, breaking out of something from many places and handling it in one. It can solve the issue of having to do the same thing In 5 places. Id prefer a mix of both when appropriate. I want throws (not just exceptions) and errors as values, maybe an even catch-into operator.
Saying that your new will not clean up and your files handle won't be closed in C++ is like saying rust is unsafe because you can use the unsafe keyword
no, on my comp 2 class the teacher commonly used new
@@eldritchcookie7210 Your comment is the equivalent of "English is a very simple language, because all my teacher knew and could teach me is how to say 'Hello my name is ...' ".
He's incompetent, and at best, he's teaching how C++ was used in the early 90s, including giving his students many bad habits. Unfortunately there's many of them.
In C++ you MUST use RAII if you enable exceptions. Even if you disable exceptions you should still use RAII :).
What I get from it is that Prime likes errors as values only because you have to encode them in the type system (like as a union type or something) whereas exceptions are essentially untyped. Even though im on team exceptions I kinda agree.
Remember this "colored functions" article a while ago? It was about async functions and how if you want to use a single async function the entire stack has to be async. We should do the same thing for exceptions in typescript. If you import a function that didn't type what it can throw then you have to catch all exceptions. It would be like a function returning any type from a vanilla JS function. However if you import a typed function (in this new system, with typed exceptions) you only have to catch those specific errors. And if you don't catch them all, then your function automatically get typed as throwing all the remaining errors.
But colored functions are bad. Why are you suggesting doing anything like that? Regularity is preferable almost always.
Prime is slowly moving to the functional cult and I welcome him
The fundamental problem with errors as values is the programmer _can_ just { some_function_might_error();} and disregard the return. Java's approach to exceptions, where the caller _must_ either convert the exception to runtime exceptions, mark the calling function with the same "throws" list, or handle the exception means you _actually_ have to do something with exceptions.
And yes, you see this in real world code _all_ the time. How often does someone call malloc() without checking if the return is positive?
On Linux, realistically, malloc can't return null
@@thewhitefalcon8539 echo 2 > /proc/sys/vm/overcommit_memory
Not too uncommon when you're running Linux on _tiny_ embedded systems. Check out the Linux business card for the type of soc where this matters.
In rust, it actually a compiler warning if you don't handle the result type. But anyways in java you can just write an empty try catch and i would have the same problem
@@yuitachibana8829 C/++ also can give warnings on unused results, either an unused result or an unused local if the result is captured to a local and then not checked. Having that on by default would be an improvment.
Yes, you can have an empty error handler, but unless you are using an IDE that _generates_ an empty handler for you (Eclipse used to do that), the programmer _must_ type that in. Doesn't help if they are intentionally lazy, but does help if they get distracted or are simply unaware of the possible exception.
[[nodiscard]] int foo (); // C++17, strong suggestion for the compiler to issue a warning if not used
a lot of games will reserve some amount of memory as a "just in case", that is either freed later down the line in production when memory budgets get tight, or for OOMs so you can open a file handle and have a big enough buffer to write crash data
What Prime is complaining about is really just a language allowing you to basically forget that exceptions are thrown. That's poor language design and is not a problem with exceptions themselves.
Java requires exceptions to be caught or thrown and that's required on a syntax level to even compile. It's one of the good things about Java. Now, if someone decides to throw a generic Exception all the way to the top of your application and you can no longer see where something is thrown or where something is coming from, then that's just a pure skill issue.
I do wish more languages were better at telling the developers that a function can throw though. Errors as values enforces this, which is a good thing. I just don't believe it's a problem with errors as values vs. exceptions.
it would;ve been nice if Java didn't also allow you to forget that exceptions exist until one jumps you. it doesn't help that try blocks tend to be clunky, and there are 0 options outisde try blocks in java. Kotlin has more options in the way try blocks are used which greatly expands their utility, you can even implement Rust's result pattern in kotlin using try.
@@warpspeedscp Huh? Java doesn't let you forget exceptions. They are always declared with the throws keyword. There are errors that come from unexpected behavior in case you haven't checked for them, such as division by 0, but that's not an exception, that's a runtime error.
If that's not what you're talking about then please tell me.
@@CottidaeSEA sure it does, many libraries and applications have methods and apis that dont explicitly declare what errors could occur in their use, which leads to uncaught exceptions. That isnt necessarily java's fault, but people are never forced to properly enforce or comply with the contract of an api, such as what error states exist. How many libraries out there explicitly use checked exceptions to signal error states?
@@warpspeedscp The only time you can have an unchecked exception in Java is if you're throwing a RuntimeException. Those are supposed to be unrecoverable errors and I rarely encounter them. You can catch them, but they shouldn't even be thrown to begin with unless something is extremely wrong.
What about Erlang's let it fail model? Code that fails should just be restarted and usually recovers.
...Should just be restarted?! And potentially lose all state?
@@stysner4580 it's functional for a reason
That is only when something unexpected happens. If you are going to crash each time doing the same thing then you are doing it wrong and you should handle the error.
It is not crash and forget.
@@stysner4580
Code should be reentrant when you use these languages.
Sounds like exceptions
The problem that exceptions solve relatively well is that it varies a lot what level is the correct one to resolve the error. You just let it throw until it reaches the level that knows WHY the operation was done (and usually knows the context and what error to display and how). Errors-as-values can lead to each level just adding a message resulting in a stack of "Error: method FOO returned error: Error BAR returned error...". Also depends on running envinronment, e.g. OOP, React, Node what makes most sense.
PS. Also I like "crash early, crash hard"-style of initially handling those errors you know for certain how to resolve and let the rest to blow up (assuming you can do this without pissing off customers). Then you look at the logs and architect the error handling, which ones you understand, how do you handle them as a whole.
Disappointed by the many bad faith arguments in this video. The point of the fibonacci example was to demonstrate that exceptions are much faster than error handling in a tight loop. Of course it is a somewhat contrived example because nobody calculates fibonacci numbers that way but is it really hard to glean the point being made? You can easily imagine some other function that was doing some computation in a tight loop and you needed to check for e.g. integer overflow. The point is that the version of the code with exceptions would be much faster than the version with error values.
that could have been just parser
@@kurku3725 Exactly. And then he bemoans how it is only because more allocations are more expensive than less allocations, which is like, duh, that is the entire point. With exceptions you have to do less allocations on the happy path and how can that can be a bad thing from a performance point of view?
Of course you could have a language compiler or runtime that handle error types in a special way so that you would get the same performance as exceptions, but afaik none of the current languages that do error handling this way implement that. And even if they did, your error "values" now have different semantics than other types in the language, so you just invented exceptions with a different syntax.
It sounded like at one point you said something to the effect of "well, of course you'll still want a try...catch at the top level to make sure you can catch errors that you weren't expecting", but this means you're using two error handling paradigms instead of one. The boilerplate, while you said it doesn't matter, is still logic that your brain has to parse and occupies screen real estate that other code could occupy instead. If you're blending monads and simple bool returns that's an additional complication to track. Using try...catch also means you're able to avoid checking the boilerplate from return values and only handling things as they become problematic -- the goal of error handling isn't to handle all errors, only errors that make a difference to outcomes.
Prime still doesn’t understand what Hawktua is. He’s saying he’s enthusiastically servicing that article!
DjRio0001 still doesn't understand that a teenager from 2024 did not invent the onomatopoeia for "hocking" for a larger spit.
Naaaaaah, hawking is for loogies, not for spit! Don't do that for any pleasuring!
A good rule of thumb to decide whether to use an exception or a return value in the implementation of a function is to ask the question:
Do I expect my IMMEDIATE callers to make decisions based on that?
If yes, the return value is usually a better way. Otherwise, the exception is usually the way to go.
errors aren't necessarily exceptional
Again the Erlang let it crash model mixed with prolog roots is the best of both worlds.
I am obsessed with the Fibonacci sequence, and C++ was my first love. Seeing a runtime, exception-laced implementation instead of a const lookup array generated at compile-time hurt me deeply. LOL.
it calls the destructor !!!
So what if the destructor should not be called in case of an error?
@@stysner4580
Bad system design.
That's with RAII
I prefer perfect code.
exceptions are superior for at least 2 reason
1. often how to handle failure is several calls up the stack and your stupid function doesnt know the context to do anything meaningful
2. any language supporting exceptions also supports errors by value, no one stops you from returning a tuple
you should always have the options
and yes panic is the most moronic way to do anything
Panic and terminate the process is a pretty good option when you are writing to a hard drive full of family photos without any backup and one of your assertions don't hold anymore.
@@youtubeenjoyer1743 you are basically saying we should assume programmers are incompetent and make programming languages fail at the first sign of trouble...
@@krellin no, I’m not saying that. I suggest you try to read next time.
I wouldn't say that "exceptions are superior", but I have used both extensively, and I firmly agree with your first point. From a practical standpoint, loss of context in errors is definitely a huge struggle when debugging errors in golang or rust. Always having a full backtrace is very convenient. And by the way, there's almost always some libraries in "error-as-value" languages that try to reimplement backtraces on errors, usually through a very dubious way (like codegen and such).
Regarding your second point, one could argue that it's better to have a single idiomatic way to handle errors, because if half libraries use exceptions and the other half use error as values, then it starts being very inconsistent in your programs, and it's harder to work with.
I think both approaches have pros and cons.
@@Madinko12 fine its not superior as its not the same thing, both are tools and competent languages should offer both. I do not find it problematic with one library doing one or the other.
If library wants to use values fine, you know it from the return type... if it can throw exceptions chances are you already are handling anything unexpected few calls up the stack before you went into library code.
My favourite thing about exceptions is that they allow you to define the place they should be handled.
Say we have function A, B, C that calls D and then E. In E we do a network call that can fail. What happens when E fails is slightly different in A, B and C. Exceptions allows us to pass through D and keep things generic.
Granted the issue I find is more pressing is when a Junior dev doesn't know D can fail and uses it in function F. Then D fails, F doesn't handle it and F causes the entire system to crash.
This issue doesn't really go away with C style return values. It could be argued to be worse since it may cause F to fail in an unrelated way.
The biggest problem with exceptions is runtime polymorphism. Ironically, golang has the exact same problem with its Error interface, but without any of the benefits.
Java has reflection to check the type of the expression and Wildcard to restrict the type, that's not a good point in my opinion
nice pfp
HARD disagree. There is a reason why Rust `Error`s have a downcasting API.
This is over one hour of Prime continuously misunderstanding the purpose of stack unwinding.
Correctly up intermediate state (e.g. running destructors in C++) is the whole point. It’s why malloc and new are discouraged, and are meant to be tucked away in classes that destruct correctly.