Problems with C++ Exceptions
Postedabout 2 months agoActiveabout 2 months ago
marler8997.github.ioTechstory
heatednegative
Debate
80/100
C++Exception HandlingProgramming Languages
Key topics
C++
Exception Handling
Programming Languages
The article criticizes C++ exceptions, but commenters largely disagree, pointing out the author's misunderstandings and suggesting alternative approaches to error handling.
Snapshot generated from the HN discussion
Discussion Activity
Very active discussionFirst comment
35m
Peak period
67
0-6h
Avg / period
12
Comment distribution108 data points
Loading chart...
Based on 108 loaded comments
Key moments
- 01Story posted
Nov 12, 2025 at 12:44 AM EST
about 2 months ago
Step 01 - 02First comment
Nov 12, 2025 at 1:19 AM EST
35m after posting
Step 02 - 03Peak activity
67 comments in 0-6h
Hottest window of the conversation
Step 03 - 04Latest activity
Nov 15, 2025 at 3:23 AM EST
about 2 months ago
Step 04
Generating AI Summary...
Analyzing up to 500 comments to identify key contributors and discussion patterns
ID: 45896733Type: storyLast synced: 11/20/2025, 5:39:21 PM
Want the full context?
Jump to the original sources
Read the primary article or dive into the live Hacker News thread when you're ready.
That is, a `throw` statement in Swift simply returns an `Error` value to the caller via a special return path instead of the normal result.
More explicitly, a Swift function declared as:
Could be read as More here: https://github.com/swiftlang/swift/blob/main/docs/ErrorHandl...Java experience taught us that, when writing an interface, it is common not to know the exception type. You often can’t know, for example, whether an implementation can time out (e.g. because it will make network calls) or will access a database (and thus can throw RollbackException). Consequently, when implementing an interface, it is common in Java to wrap exceptions in an exception of the type declared in the interface (https://wiki.c2.com/?ExceptionTunneling)
One could argue Rust is slightly better than Java, because in Rust there are no unchecked exceptions. However, in Rust there is panic, which is in a way like unchecked exceptions, which you can also catch (with panic unwinding). But at least in Rust, regular exceptions are fast.
But you get the same with checked exceptions in Java. Yes, an interface will say foo can only throw FooException, but if you want to do anything when you get a FooException, you have to look inside to figure out what exactly was wrong, and what’s inside that FooException isn’t limited.
A later version of the library may suddenly throw a FooException with a BarException inside it.
So as a user you could check for a generic file_not_found error, and if the underlying library uses http it could just pass on the 404 error_code with an http_category say, and your comparison would return true.
This allows you to handle very specific errors yet also allow users to handle errors in a more generic fashion in most cases.
[1]: https://www.boost.org/doc/libs/latest/libs/system/doc/html/s...
1a. yes, there was some error 1b. there was an error--throw another local error and encapsulate the caught error 2. treat result of throwing call as `nil` and handle appropriately
I don't think typed throws add anything to the language. I think they will result in people wasting time pondering error types and building large error handling machines :)
When I used Java, I found typed exceptions difficult to reason about and handle correctly.
https://github.com/swiftlang/swift-evolution/blob/main/propo...
I say limited because the compiler doesn't (yet, as of 6.2) perform typed throw inference for closures (a closure that throws is inferred to throw `any Error`). I have personally found this sufficiently limiting that I've given up using typed throws in the few places I want to, for now.
That’s a huge limitation when writing libraries. If you have an old function that declares that it can throw a DatabaseError, you can’t e.g. add caching to it. Adding CacheError to the list of throwable types is an API breaking change, just like changing a return type.
Swift has typed errors now, but they shouldn’t be used carefully, and probably not be the default to reach for
> if err != nil return err
When you call code that can throw (return an error via the special return path) you either have to handle it or make the enclosing context also throwing.
Assuming `canThrow()`, a function that might throw an `Error` type:
Call canThrow(), don't handle errors, just rethrow them Alternatively, catch the errors and handle them as you wish: There are a few more ways to handle throwing calls.. For example- `try?` (ignore error result, pretend result was `nil`)
- `try!` (fail fatally on error result)
It's funny how often functional programming languages lead the way, but imperative languages end up with the credit.
Java's checked exception is just an (very anti-ergonomic) implementation of tagged union type.
I think FP receives a lot of "credit."
Albeit the "pure" FP languages aren't popular, because 99% FP is really hard.
But they CAN be caught with a standard catch which may be non-intuitive if you don't know about them, and about that, in advance.
It's a shame. Were I designing a "low-level" or "systems" language, rather than put the cart before the horse and pick error results or exceptions, my litmus test would be how to make handling OOM as easy as possible. Any language construct that can make gracefully handling OOM convenient is almost by definition the fabled One True Way. And if you don't have a solution for OOM from the very beginning (whether designing a language or a project), it's just never gonna happen in a satisfactory way.
Lua does it--exceptions at the language level, and either setjmp/longjmp or C++ exceptions in the VM implementation. But for a strongly typed, statically compiled language, I'm not sure which strategy (or hybrid strategy) would be best.
Depending on the OS you likely won't even get the chance to handle the error because the allocation never fails, instead you just over commit and kill the process when physical memory gets exhausted.
I guess what I'm trying to say is designing your language's error handling primitives around OOM is probably not a good idea, even in a systems programming language, because it's a very rare class of generally unrecoverable error that only very carefully designed code can recover from.
Anyhow allocating isn't that absurd either. It keeps the size of Result<T, E> down by limiting the 'E' to be a pointer. Smaller Result<T, E> can help the compiler generate better code for passing the return value, and the expectation is the error path is rare so the cost of allocating might be outweighed by the lighter Result<T, E> benefits. Exceptions take a similar standpoint, throwing is very slow but the non-exceptional path can (theoretically) run without any performance cost.
I want to say the main driver for those right now is Rust for Linux since the "normal" panicking behavior is generally undesirable there.
This is why I think checked exceptions are a dreadful idea; that, and the misguided idea that you should catch exceptions.
Only code close to the exception, where it can see through the abstraction, and code far away from the exception, like a dispatch loop or request handler, where the failure mode is largely irrelevant beyond 4xx vs 5xx, should catch exceptions.
Annotating all the exceptions on the call graph in between is not only pointless, it breaks encapsulation.
Just include the file name in the error message. And all of this is predicated on logging errors, which is not at all user-friendly, and not remotely acceptable in GUI applications.
That means that the top level handler can at best log a vague message.
That in turn means you must log along the way where you have more precise information about the failure, or you risk not having enough information to fix issues.
And that in turn means you must have lots of redundant logging, since each point in the stack doesn't know whether the abstraction it's invoking has already logged or not, or encapsulation would be violated.
Yes, that's the idea. You split the information into a diagnostic with stuff you deal with now and data that the upper layer will handle. The intersection between the data in these two things should be empty.
> And that in turn means you must have lots of redundant logging, since each point in the stack doesn't know whether the abstraction it's invoking has already logged or not, or encapsulation would be violated.
No. You print a diagnostic, about what exactly THIS layer was trying to do, you don't speculate what the upper layer was trying to do and try to log that. Every layer knows the lower layer has already logged the primary error, because an error object exists, and it also knows that the upper layer will print a diagnostic about what was the intention, so it only prints exactly what the error was in this layer.
> doesn't know whether the abstraction it's invoking has already logged or not, or encapsulation would be violated.
It knows that the lower layer has logged all stuff that that layer considered to be important, and that none of the data that is available to this layer was logged, since that is the responsibility of the caller.
An example:
Depending on the log level, you wouldn't show the later diagnostics. If there is a debug flag set, you can also add the function/line information to every step, not just to the last. If you are outtputing to stuff like syslog, you would put each diagnostic on its own line.I think our disagreement is less about error handling and more about logging policy. I favour a logging policy which lets you understand what the code is doing even if it doesn't error out; this means that you don't need to log on unwind. You favour a logging policy specific for errors.
My position is that your position ends up with less useful context for diagnosing errors.
I use "unwinding" for diagnostics even in the happy case.
Unable to create the media database. Failed to upgrade the database from version 3 to version 6. SQL Query failed. Syntax error at column 29 near "wehre". ("SELECT id`Thumbnail from tThumbailIndex wehre idFile=?").
(Messages are pre-pended to inner errors as one progresses up the stack). Yes, a gruesome error message. But it's a gruesome error condition.
I've also used inner exceptions in a large Enterprise product, and was very pleased with the result. (Java has them, .net has them, C++ and Typescript can have them if you want them). At the top level, the error message goes:
(each successive error message contained in a successive inner exception). The advantage: no need to assemble multiple lines of logged errors from a logfile that end-users really don't want to be digging around in.A more prosaic and actually end-user-helpful chained message might be:
Awful stuff.
The filename to be opened is likely passed as a string in an argument, so it IS present in the function interface contract.
For example, it plays poorly with generics. (Especially if you start doing FP, lambdas.)
If Java added union types, it wouldn't be a big deal, but AFAIK this is still a limitation.
The starting example is how I'd do it in C:
```
void f(const char* p) // unsafe, naive use
{
}```
Wouldn't the simpler solution be ensuring your function doesn't exit before release? All that c++ destroyer stuff appears somewhat unnecessary and as the author points out, creates even more problems.
Destructors are a higher-level and safer approach.
That's a C++ specific limitation, it works just fine in C.
As an aside, it is one of the reasons why I finally decided to let go of C++ after 20 years of use. It was just too difficult to teach developers all of the corner cases. Instead, I retooled my system programming around C with model checking to enforce resource management and function contracts. The code can be read just like this example and I can have guaranteed resource management that is enforced at build time by checking function contracts.
I include function contracts as part of function declarations in headers. These take the form of macros that clearly define the function contract. The implementation of the function evaluates the preconditions at the start of the function, and is written with a single exit so the postconditions can be evaluated at the end of the function. Since this function contract is defined in the header, shadow functions can be written that simulate all possibilities of the function contract. The two are kept in sync because they both depend on the same header. This way, model checks can be written to focus on individual functions with any dependencies simulated by shadows.
The model checks are included in the same project, but are separate from the code under instrumentation, similar to how unit tests are commonly written. I include the shadow functions as an installation target for the library when it is installed in development mode, so that downstream projects can use existing shadow functions instead of writing their own.
Also Bjarne's control of C++ is quite limited, and he is semi-retired, so asking him to "fix his language" is fairly misguided. It's designed by a committee of 200+ people.
Anyway what you want seems to be to not use exceptions, but monads instead. These are also part of the standard, it's called std::expected.
However, where the language and it's objects and memory ends and the external world begins, like files and sockets... that's always tricky.
Occasionally I do like to use auto or lambdas from C++11 but even then I have to remember more rules for initialization because braces were introduced.
But now the language packs so much complexity that I can't be sure I understand all of the code I'm looking at.
My usual quality standard for production code is that it doesn't just need to be correct, it needs to be obviously correct. So that's a problem.
Doubly so when the other programmers on the team aren't C++ geeks.
Do you mind to elaborate what you believe are the misunderstandings? Examples of incorrect/inaccurate statements and/or an article with better explanations of mentioned use cases would be helpful.
> it's called std::expected
How does std::expected play together with all other possible error handling schemas? Can I get unique ids for errors to record (error) traces along functions? What is the ABI of std::expected? Stable(ish) or is something planned, ideally to get something C compatible?
Regardless, in his example, he could achieve what he wants by wrapping the try in a lambda, and returning either the value from try or nullopt from catch. But clearly, that's just converting exceptions to another error-handling mechanism because he isn't doing it right.
He claimed that not handling an exception causes the program to crash, that's just plain incorrect. To be fair many people use the term "crash" liberally.
std::expected or equivalent is often used with std::error_code, which is an extensible system (error codes are arranged in categories) that among others interops with errno.
Tell that to Bjarne!
Exceptional C++, by Herb Sutter is an excellent resource that explains this. It’s quite outdated these days but the core concepts still hold well.
When done well, most of your code can be happy path programming with errors/invalid state taken care of mostly automatically.
However it’s also very easy not to do this well, and that ends up looking like the suggestions the author of the article makes.
And this is coming from someone that dislikes exceptions.
I'll add that inspiration for the article came about because It was striking to me how Bjarne's example which was suppose to show a better way to manage resources introduced so many issues. The blog post goes over those issues and talks about possible solutions, all of which aren't great. I think however these problems with exceptions don't manifest into bigger issues because programmers just kinda learn to avoid exceptions. So, the post was trying to go into why we avoid them.
If you really want to handle an error coming from a single operation, you can create a new function, or immediately invoke a lambda. This would remove the need break RAII and making your class more brittle to use.
You can be exhaustive with try/catch if you're willing to lose some information, whether that's catching a base exception, or use the catch all block.
If you know what all the base classes your program throws, you can centralize your catch all and recover some information using a Lippincott function.
I've done my own exploring in the past with the thought experiment of, what would a codebase which only uses exceptions for error handling look like, and can you reason with it? And I concluded you can, there's just a different mentality of how you look at your code.
Their main complaint about exceptions seems to be that you can’t handle all of them and that you don’t know which you’ll get? If we compare this to python, what’s the difference here? It looks like it works the same here as in python; you catch and handle some exceptions, and others that you miss will crash your program (unless you catch the base class). Is there something special about C++ that makes it work differently, or would the author have similar problems with python?
The third problem (RAISI) is a C++ specific problem that Python doesn't have. Partly because in Python try/catch doesn't introduce a new scope and also partly because Python tends not to need a lot of RAII because of the nature of interpreted languages.
I found this video a fascinating take on comparing C++ to Python if you haven't seen it: https://www.youtube.com/watch?v=9ZxtaccqyWA
I would expect yes. It is true, that in a lot of modern languages you need to live with that dynamism. But to people used to C, not knowing that the error handling is exhaustive, feels deeply uncomfortable.
What I dislike is having a mechanism to skip 10 layers of bubbling deep inside call stack by a “mega” throw of type <n> which none of the layers know about. Other than
But what I would really want is noexcept regions:
i.e. a way to mark regions of code that cannot deal with unwind and must only call non-throwing operations.Manage classical C resources by auto-cleanup variables and do error-handling the normal way. If everything is OK, pass the ownership of these resources from auto-cleanup variables to C++ ctor.
Note this approach plays nicely with C++ exception, and will enter C standard in the form of `defer`.
Unfortunately, even the Linux kernel is no longer C because they use GCC compiler extensions (and are currently discussing adding MS ones too).
Exception safety has a lot of problems in C++, but it's mostly around allowing various implicit operations to throw (copies, assignments, destructors on temporaries and so forth). And that does come down to poor design of C++.
> and no stack trace
That loads of error messages, meaning every layer describes what it tried to do and what failed, IS a user readable variant of a stack trace. The user would be confused with a real stack trace, but nice error messages serve both the user and the developer.
You shouldn't do manually what you can automate.
Boilerplate can make you feel productive and can give you warm fuzzies inside when you see lots of patterns that look familiar, but seeing the same patterns over and over again is actually a smell; it's the smell of a missing abstraction.
Also you can never have comments in the code, because what you would write into the comment is now in the diagnostic itself. That means you can also turn on DEBUG_LEVEL_XXL and get a nice description, what the program actually does and why.
> This is error-prone
Why is it error-prune. You receive an error of one type and need to convert it into an error of another type. You need to handle that or the compiler will bark.
> obscures the logs
How does it obscure the logs?
If there's extra information you need to know about what's going on, you probably want to log it in non-error cases too, because it'll contextualize an error before the error occurs. You can't log on unwind for decisions made in routines that were invoked and returned from, and are no longer on the stack, in the error case.
> Why is it error-prune. You receive an error of one type and need to convert it into an error of another type. You need to handle that or the compiler will bark.
This is a choice you've made, not something inherent to unchecked exceptions. My argument is that you should not generally change the type of the error, and let polymorphism cover the abstraction. It's error prone because it's code you need to write. All code you write is on the cost side of the ledger and liable to contain mistakes.
> How does it obscure the logs?
You get lots of error log messages for a single error.
> In addition, you would need to allocate on a failure path, which sounds like a nightmare.
I wanted to address this separately. Allocating on the failure path is only a real problem in OOM or severe memory corruption scenarios, in which case logging may not be available and the best course of action is probably to abort the program. In this case, you want the logs that led up to the abort, rather than trying to log positively under severe memory conditions.
Are you a Go user by any chance? I've stayed away from Go precisely because of its boilerplate-reliant error handling mechanism. Or if you're stuck with C++ (with or without exceptions), my commiserations.
Yes, but when you want to generate a diagnostic from that stack trace, you basically need to branch on all the possible internal states of internal layers at the point you catch the exception. So either you are incapable of generating detailed diagnostics or you essentially model the whole behaviour in a single place. Also the point where the error messages are generated is now completely removed from the place where the error did occur. This sounds like a nightmare to maintain and also means that the possible error messages aren't there as documentation when reading the code.
> If there's extra information you need to know about what's going on, you probably want to log it in non-error cases too, because it'll contextualize an error before the error occurs. You can't log on unwind for decisions made in routines that were invoked and returned from, and are no longer on the stack, in the error case.
I never said, that you can only use this for error cases. In fact what is an error and what not, is not defined in a single layer. For example a failure to open a file will be an error in a lower layer, but for an upper layer, that just means that it should try a different backend. Or a parsing error is fatal for the parser, but it might mean that the file format is different and the next parser should be tried, or that the data is from the network and can simply be discarded. An empty field can be normal data for the parser, but for the upper layer it is a fatal error.
> This is a choice you've made, not something inherent to unchecked exceptions. My argument is that you should not generally change the type of the error, and let polymorphism cover the abstraction.
Then either the reported errors are completely unspecific and unactionable or you are leaking implementation details. When you want to handle every error specifically and not leak implementation details, you need to handle it locally. When you want to know that you handle all cases, unchecked exceptions are unsatisfying. In my opinion programs should know what is going on and not just say "my bad, something happened I can't continue". That does not lead to robust systems and is neither suitable for user transparency nor for automated systems.
In my opinion software should either work completely automated or report to the end user. Software that needs sysadmins and operators at runtime is bad. That doesn't mean that that never occurs, it will, but it should be treated as a software defect.
> You get lots of error log messages for a single error.
Yes, but this describes the issue at different layers of the abstraction and in my eyes the whole thing is a single error message. Neither the fact that resource X isn't available nor the fact that some connection was refused, is a complete error description in isolation. You need both for a (complete) error description.
> Allocating on the failure path is only a real problem in OOM
Yes, but first I don't like my program to act bad in that case, and second, it is also a nightmare for predictable ownership semantics. I generally let the caller allocate the error/status information.
> Are you a Go user by any chance?
I have never used Go, not even tried, but what I read about the error mechanism appealed to me, because it matches what I think is a good idea and do anyway.
> Or if you're stuck with C++ (with or without exceptions), my commiserations.
I don't feel that unhappy with that approach. I think this is a design and architectural decision rather than a language issue.
> with or without exceptions
Currently, definitely without, because it isn't even available when targeting a free-standing implementation (embedded), but I also don't prefer them, it makes for unpredictable control flow and makes it hard to reason about sound-, complete- and exhaustiveness.
You seem to have the impression, that you can just panic and throw a stacktrace. That might work fine for a program running in the terminal and targeting developers, but it is not acceptable for end users nor for libraries. I also know programs that just output a stacktrace and crash. That is stupid. I mean I understand what is going on, because I am a developer, but first I am not familiar with every codebase I use, and second the average end user is not able to act on any of that and will be angry for good reason when it's documents are gone, data is corrupted or even the workflow is interrupted. I also don't perceive a network error, a file (system) issue or OOM to that rare for it to be acceptable, to just ignore it. I should be part of normal program behaviour.
Guess Java does that, not much experience in Java here.
All that is needed is a better File_error type that includes the error that happened.
Is this an LLM prompt?
https://yosefk.com/c++fqa/
If you want to simplify the callsite, just move the exception handling to a higher scope. I can admit it’s a little irritating to put a try/catch in main but it’s trivial to automate, and most programs are not written inline in main.
The main problems I see with destructors have to do with hidden control flow and hidden type information. That said, hiding exceptional control flow from the mainline control flow of a function is also a useful feature, and the “exception type” of any given function is the recursive sum of all the exception types of any functions or operators it calls, including for example allocation. That quickly becomes either an extremely large and awkward flat sum or an extremely large and awkward nested sum, with awkward cross-namespace inclusion, and it becomes part of the hard contract of your API. This means, transitively, that if any deep inner call needs to extend or break its error types, it must either propagate all the way up into all of your callers, or you must recover and merge that error into _your_ API.
For _most_ usecases, it is just simpler to implement a lean common interface such as std::exception and allow callers who care to look for more detailed information that you document. That said, there is a proposal (P3166 by Lewis Baker) for allowing functions to specify their exception set, including via deduction (auto):
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p31...
But this shows problems with C++ exceptions, C++ codebases are literred with bad exception usage because "normal" programmers don't get it. Most of didn't get it for long time. So, they are complex enough that their usage is risk.
Anyway, IMO C++ exceptions have two fundametal problems:
* lack of stacktrace, so actually lack of _debugabbility_, that's why people try/catch everything
* destructor problem (that actually there are exceptions tht cannot be propagated further and are lost
The page about try/catch explains it well https://hexdocs.pm/elixir/try-catch-and-rescue.html
- Correctness: you don’t know if the exception type you’ve caught matches what the code throws
- Exhaustiveness: you don’t know if you’ve caught all exceptions the code can throw
But that's actually not a problem. Most of the time you shouldn't catch a specific exception (so always correct) and if you are catching then you should catch them all (exhaustive). A better solution is merely:
That's all you need. But actually this code is also bad because this function f() shouldn't have a try/catch in it that prints an error message. That's a job for a function (possibly main()) further up the call stack.