Completely agreed, ideally you need to be able to define “child” error types that declare their own fields with extra data, allowing consumers to handle these errors in more ways than just rethrow them.
But then you throw performance out of the window if you have some hot loop that needs to do error handling and your error object doesn't fit in a register anymore
you're back to multiple error scheme depending on the parts of your app
As you should be, if your app is that performance-sensitive. You use the idiomatic/maintainable solution by default, and a more performant solution where it matters.
I would prefer to have a smart compiler that understands how I use the function result and optimizes it. In that case, the function returns more detailed information, and the caller decides whether this information is needed and makes ignoring it zero cost.
I'm a huge Rust advocate, but it doesn't automatically manage memory without GC. It's RAII based. It's just that most of the time you don't need to deal with that yourself since the bulk of it (sometimes all of it in some systems) is handled by standard library types.
you throw performance out of the window if you have some hot loop that needs to do error handling and your error object doesn't fit in a register anymore
Just make your "error object" a pointer that points to null if there's no error, and otherwise points to a union/struct/instance that holds the error info.
To add to this, it's also easy enough to pre-allocate all or most of an error object before it is ever returned, if the loop must remain as hot as possible. Maybe even make it static to avoid a heap allocation entirely and have a flag in the error object to indicate that it's not heap allocated.
You could also keep circumstantial flat error codes, at least on x86, by using the non-canonical address range, provided that you never dereference the pointer. A sentinel-like value such as #define SIZE_TOO_SMALL ((CleverError*)0xDEADDEADDEADDEADULL) would be sufficient. Portability and adherence to the C standard is not guaranteed. No refunds.
Heap allocated errors make this a non-issue, the error pointer still fits in a register so you can still do hot loops with error handling. The error check just needs to be an error check that'll be negative in roughly 100% of cases.
Of course hot loops with error handling where errors happen in the typical case would be problematic. But if your fast path includes an error being produced, you're doing it wrong.
Lots of errors are good and expected. For example, validating untrusted input, which is most input, should fail in ways that ideally aren’t any slower or faster than the happy path.
The reason is that timing attacks exist. If an attacker can determine if they did something right, or triggered an exceptional error path, you are leaking information to them. In the best case, they can DoS you. In the worst case, all of your moneys is gone.
If exceptions were part of the function return interface and had nice syntax like ? to explicitly rethrow the error so that the user acknowledges it's a falliable function, I would love exceptions.
If exceptions were part of the function return interface and had nice syntax like ? to explicitly rethrow the error so that the dev acknowledges it's a falliable function, I would love exceptions.
Sounds like Java's checked exceptions and people did not like them.
Yes and no. Optional/Either/Result/Monads have slightly different purpose. Even in Rust you have panic ;) Anyway, "discovering" in 2026 that error codes are not enough is just ridiculous. I mean it's a good observation, but it's also common knowledge for at least 40 years.
I agree that panic is a different use case. Result/Either is closer to checked exceptions specifically. And yes, it's strictly better than those. I go into detail in the link above.
This made me instantly think of java & the endless stack traced it's wont to display at the drop of a hat. Antipodal to flat error codes and not pretty either.
For the purpose of nesting errors and providing extra details for recovery or logging, Java exceptions are actually pretty great. Your issue is simply with the default display style. You can have a similar nightmare with nested error values in Rust, if you print them using the verbose Debug interface, instead of the Display interface meant for the users.
No they are not great. They don't provide any data from function args or local variables. Just because an exception was thrown somewhere in a loop doesn't tell you what was being processed on that iteration. And even if you include that info in the exception message, it's still not of much help because you also usually need that info on other stack frames. Which forces the developer to catch and rethrow everywhere, defeating the purpose of stack traces completely (what's the good of an automatic stack trace if one has to enrich that stack trace manually).
As a Java developer, I dislike stack traces with a passion because they are only useful in the most trivial cases. Any looped code and a stack trace doesn't do crap to help investigate.
I've thought about the issue of error reporting in the past, but never implemented it. I think you need an Error object, which is a stack of errors, e.g.:
file read error
could not read mapping.db
failed to convert currency
shipping calculation failed
could not complete transaction
The user is told "could not complete transaction" with a Details button which shows the next level down. If you just pass the original error all the way up, the user is told "file read error", which is next to useless.
Could be security issue: you can break system by analyzing error stacks
Only if you pass the entire stack to the user. You could log the entire stack and pass only the top two items to the user.
Besides, just how much of a security breach can it possibly be? I regularly see entire exception backtraces in C# WebApps and in almost every Java app too, but all the exploits we see don't appear to be as a result of those.
If you punt a stack trace at me, and I can see from it that you're using a particular library to, for example, parse CSVs from user input, I can then tailor my attacks to focus on exploits in that library.
It's not necessarily knowing the stack itself that helps attackers. It's what you know when you know the stack that helps.
I don't disagree that it is a security issue, I just think that on a scale from 1-10, it's probably a 1.
After all, just knowing that the server is written in C# already narrows down your potential library targets to maybe 2 or 3 for each library (JSON parsing, or third-party requests, or ORM). Same for almost any other language. In some non-mainstream language, you can be sure of a single library being used (Haskell, Ocaml, etc).
Everything is a security issue, and it becomes a trade-off between having users submit an error message that requires no deep dive into your logs and having a malicious actor use that info to target your system.
After all, just knowing that the server is written in C# already narrows down your potential library targets to maybe 2 or 3 for each library (JSON parsing, or third-party requests, or ORM).
Right, but do you know which version is being used? There's a reason OWASP talks about this stuff. If you're doing something important, with other people's data, you can't afford to expose a bigger attack surface than you have to, and reducing the visibility of the attack surface helps.
If you want to access those the trace, great. Log it, give it a GUID, report the GUID to the user. Or a truncated code if you don't want to overwhelm them with a long string of characters. It's also a lot easier to copy that trace out of a database than it is a photograph of a screen covered in schmutz.
Right, but do you know which version is being used? There's a reason OWASP talks about this stuff. If you're doing something important, with other people's data, you can't afford to expose a bigger attack surface than you have to, and reducing the visibility of the attack surface helps.
I agree; defense-in-depth must necessarily include obscurity!
f you want to access those the trace, great. Log it, give it a GUID, report the GUID to the user. Or a truncated code if you don't want to overwhelm them with a long string of characters.
This is only for the use-case where the user sends you the error message. I was including the use-case of "User examines the message and knows how to fix the problem".
For example if it is a permissions problem, they know they have to apply to their supervisor to be added to the special-people role.
If the error is "The image you attempted to link into your issue is not found: : ", then they know that someone deleted the image that was uploaded, or that they typo'ed the image name, etc.
That's why it's usually sufficient to give the user the top-level error that occurred (or maybe the highest two levels), along with the parameters that were used.
I agree; defense-in-depth must necessarily include obscurity!
Haaar haaar. There's a difference between obscurity and not showing your whole ass.
This is only for the use-case where the user sends you the error message. I was including the use-case of "User examines the message and knows how to fix the problem".
For example if it is a permissions problem, they know they have to apply to their supervisor to be added to the special-people role.
No, you can do this for everything. Every exception you catch, you can log it and display a GUID.
What you're describing here is business logic working normally. Totally different thing, you wouldn't use exceptions for that anyway.
Probably. Please tell me you're not using exceptions for authorisation checks. Please.
Please?
If the error is "The image you attempted to link into your issue is not found: : ", then they know that someone deleted the image that was uploaded, or that they typo'ed the image name, etc.
That's why it's usually sufficient to give the user the top-level error that occurred (or maybe the highest two levels), along with the parameters that were used.
Uh. We're talking about stack traces. I'm not advocating against meaningful error reporting to users. I'm advocating for suppression of stack traces.
No, you can do this for everything. Every exception you catch, you can log it and display a GUID.
Okay, that works for exceptions, but the example I have (ACL failure) isn't an exception; what does a user do with a GUID then? Mail the admin, ask "what does this mean", and have the admin respond "It means you need to ask your supervisor to add you to the correct group".
Probably. Please tell me you're not using exceptions for authorisation checks.
Why would I? ACL failure doesn't result in an exception; that's an expected result, not an exception.
Uh. We're talking about stack traces. I'm not advocating against meaningful error reporting to users. I'm advocating for suppression of stack traces.
Are we? I said "give the user the error that occurred", not the source code filename, line number and "value is NULL" information. If the error being returned is "link reference does not exist", that line alone is sufficient. All the other stuff you store/log (or discard as you see fit).
Okay, that works for exceptions, but the example I have (ACL failure) isn't an exception; what does a user do with a GUID then? Mail the admin, ask "what does this mean", and have the admin respond "It means you need to ask your supervisor to add you to the correct group".
You don't use that method for that kind of error. You handle it properly. The whole point of this was stack traces getting through to the front end, which means errors aren't being handled properly.
Why would I? ACL failure doesn't result in an exception; that's an expected result, not an exception.
Exactly. See, I was worried you were talking about using exceptions for authorisation failures because we were talking about exceptions and then you started talking about authorisation failures, which makes it sounds like you're using exceptions for authorisation failures, which is silly.
Business logic errors, including authorisation errors, should not be handled through exceptions, especially if they're user-facing. It's an entirely different type of failure condition. You handle those with properly handled feedback to the user, because you can, because you built the system to handle that scenario.
An uncaught exception is definitionally not be that you built the system to handle. So you can't actually know in advance the information you're about to show to the user. So you give them a generic error, a GUID, and log the details. That's the safe way to do that.
Sure, but that's not the point of this article. The point of this article is that an error should be rich with nested data, not just an integral flag and maybe an error string.
This comes very naturally with exceptions (exception chaining) but can be done or not done with any one of these mechanisms. That overdebated bikeshed concerns control flow and syntax and is orthogonal to this particular issue.
Right, but OP wrote this piece as a push-back against the notion of flat error codes. In the context of that discussion, you can't really leap from flat codes to exceptions without any sort of motivation in the middle.
To move people from flat error codes, to change minds, you kinda need to argue the case that points towards exceptions. It makes sense to go back to first principles here.
Depends on what you are using that error for. Error message for users should include an action that they can take to fix the error. Error messages for devs should include the inputs that resulted in the error.
Result is useless is both those situations while Exception is useful in one of them.
Flat error codes are of two types:
Descriptive error code ENOENT or EEXIST which can be mapped to a descriptive string that doesn't contain the inputs that caused the error (for example, user sees file not found, and not ./some.file.txt: file not found)
Non-descriptive error code, like open() returning a negative number on failure - that number tells you nothing and you need to use errno to get the descriptive flat error code.
The problem with error codes is not the code, but the fact that most in-house error codes don't map to a message, and even if you wanted to, you couldn't, because one library might return 20 for "invalid input" error while another library may return 20 for "record doesn't exist" error. There is no global mapping under this scheme and so error codes break down.
Either way, Flat error code is at the bottom of the usefulness stack, but it's not clear that Result is at the top.
Error message for users should include an action that they can take to fix the error. Error messages for devs should include the inputs that resulted in the error.
Result is useless in both those situations
What makes you say that? In Rust, the error types stored in Result::Err usually implement both Display and Debug traits, for those exact purposes.
Error message for users should include an action that they can take to fix the error. Error messages for devs should include the inputs that resulted in the error.
Result is useless in both those situations
What makes you say that? In Rust, the error types stored in Result::Err usually implement both Display and Debug traits, for those exact purposes.
(Emphasis mine) My point is that Exceptions give you one of those for free. Using Error types requires discipline from the developer (implement the Error type traits for a specific error), and then more discipline to avoid a panic. Flat error codes require even less code that using Error types, but requires even more discipline, discipline which the compiler cannot even catch.
I think my broader point is that Error types (or error handling) is not all that important at the language level; you can do the correct thing in any language with a little extra work. I have a pretty complex system written in C. For errors I decided up-front how to handle them:
Errors are reported using a variable argument macro; this is pushed into a global thread-safe queue, including source code line number, source filename, values of the parameters to the failing function call, and any other arbitrary message I want to put into it. The function then returns a sentinel value indicating error (NULL, maybe, or similar).
As each function checks for error after calling any function, failures can stack - effectively building a stack trace that consists of all the inputs into the actual error.
At the top-level, if the top-level operation was initiated by the user, a set of user-facing error messages are displayed.
As the queue is routinely persisted to disk, it is truncated in RAM.
Now I don't really like the mechanics of this at the language level, but at the system level this is perfect - I don't mess my metrics up by mixing up errors and logging messages and the user gets measures to take instead of "Error 0x45929: Contact your administrator". There's no Error type so calling the macro with arbitrary arguments is less work than attempting to define an error for every little function that might return something.
The reason my approach works is because there is no "recovery" attempt; there's no point, actually. "Something went wrong" == "Discard that entire code path and do the next thing instead".
I also think that if you are taking different code paths based on an Error type, you have mixed up program logic and error handling logic; if the code is recovering from something, then that thing is not an error, it's part of the business logic (example: "recovering" from a failed HTTP request by retrying - that was not an error. It becomes an error only if the retries are exhausted. Or reading a file, but creating it if it doesn't exist - once again, that's not recovering from an error, that's just business logic).
In my mind, an error is a hard stop. It stops the program from proceeding. The only acceptable way to handle it is by unwinding the call stack to a safe point and recording that the attempt to do something failed (and why it failed). The only exemption I would make is if the safe point is the very top level of the program and the program is an interactive one with a user sitting in front of it - at that point you can ask the user what to do.
The whole reason for doing an Error taxonomy (checked exceptions, error types, etc) is to enable recovery. I prefer the Erlang approach - kill that task and move on.
Using Error types requires discipline from the developer (implement the Error type traits for a specific error)
You need to manually write an impl (or use a macro like #[derive(Debug)] or thiserror), but there's no discipline involved in that. The compiler will just tell you when you try to print a non-printable error or compose it with a supposedly-printable error.
and then more discipline to avoid a panic.
No different from avoiding an inaproppriate abort or an uncaught unchecked exception in other popular languages.
you can do the correct thing in any language with a little extra work.
In theory, you can. In practice, you want. My post about exceptions links a recearch paper about broken error handling patterns that come up all the time in languages with exceptions.
if the code is recovering from something, then that thing is not an error, it's part of the business logic
I would still call that an error, but that's just a choice of words. I agree with your main idea: it's very useful to make a distinction between "expected"/recoverable errors and unexpected bugs / edge cases where it's accceptable to abort the entire task. My post about exceptions discusses this too.
The whole reason for doing an Error taxonomy (checked exceptions, error types, etc) is to enable recovery.
The Zig way, as I understand it, is to pass a pointer to a Diagnostics struct that is populated when an error happens. I think it fulfills what the author of the article wants, because you can simply have a field with the diagnostics struct from the lower-level library in the diagnostics struct of the higher-level library.
I think you should be able to generate high-quality error messages from this kind of diagnostics structure plus the program state when the error occurred for context.
FWIW I don't have much hands-on experience with this approach since I didn't use Zig too much yet, except for some toy examples.
Zig doesn't have a good library story considering comptime being something you can rely on across library boundaries with no way of knowing as the library owner that making it not comptime-compatible will break the code. If I were to reach for Zig, it'd be the same reasons I reach for C, I want build everything myself. Turns out, most of the time, I really don't want to do that, lol.
Not only is it useful to capture error codes, it's useful to be able to build a stack as well, e.g.
UnableToConfigureDatabase -> Missing Credentials File -> File not found.
If you return "FileNotFound" without saying which file it can be an issue, and likewise, saying "unable to configure database" doesn't explain which aspect also failed.
One piece of software that use flat error code pretty well is Oracle Database.
There are thousands of ORAXXXXX error codes, each one represent a well defined situation and you know what you have to check if you want to fix the problem.
So if you do it well, I think Flat error code are OK
P.S. It's probably the only positive thing I can say about Oracle Database though
I agree, they are OK in some domains. My point is that limiting yourself to error codes is a bad univeral advise. The (common) use case in the article can't be solved using plain error codes.
There are three types of information accociated with an error.
1. Information for the program to switch control flow.
2. Information for the developer to debug the problem.
3. Information for the user so they can take action.
1 belongs in an error code return value. Typically, a boolean "ok" return is the best error code. 2 belongs behind development configuration, and you should typically have external tools such as the debugger for this. 3. Belongs in a dedicated logging method.
Anything more complex than a boolean is starting to be too complex, and is begging justification. Rarely is there a legitimate use case for more than two control flows following an error. Distinguishing between a transient and persistent error is an example of where three return values would be wanted.
I agree that it's useful to distinguish these three categories of error data.
Anything more complex than a boolean is starting to be too complex, and is begging justification.
My point is that it's actually a common case. Using just a boolean or a plain error code for control flow is a bad universal advise. My use case is nothing special: displaying the user a specific message depending on which database constraint is violated. You can't solve that if your database+ORM provides you only a boolean or an error code. You need the name of the constraint as a string. Additional attached error data, used for control flow.
In C or Vala (or other GObject-supporting languages) you can use GError for this. A couple years ago it also got the ability to have extended arbitrary data (but it looks like it's kinda clunky to use).
flat error codes always felt like a design smell to me. you end up with massive switch statements everywhere adn half the codes are never documented properly. ive been on teams where we used structured error objects with a type, message, and context fields — way more useful when debugging in prod. the real issue tho is that most devs dont think about error handling until something breaks in production and then it becomes a scramble to figure out what went wrong
The problem isn't a lack of fidelity. The issue is that there primarily only two uses of error codes. One to know if something worked. And a single bit can do that.
The other is to help the customer know how to rectify their problem. One error code typically is not enough for that. Multiple also is not enough for that.
Stop thinking of users as people debugging your code and think of them just as using your tool for its functionality. You have to be able to convert your error codes into actionable messages and do it reliably.
And really few programs spend any time on doing this properly. So it doesn't matter all that much how deep your error reporting is.
Find a useful message to send to your customer. And if you want to put something in a file that can be reported to help you debug your code then do that separately. You can put a stack trace in there if you want (and if it is deemed not a security/privacy violation to do so). But don't get an idea that the detailed debugging information is going to be of help to a customer in resolving their issue. It's a help for you (your support people) instead.
I did read the post. And no, it didn't do that. It explains why one value (beyond one bit) is not enough. And it not enough because of their specific implementation, not anything else. To be fair, the article does say this is "for my use case".
All you have to do is preserve enough information to be able to create a useful message to give to the user. Even a mere 32-bit value gives you enough fidelity for 4 billion different error cases. 4 billion different root error cases. You don't actually have to go to multiple values to do this.
The post proposes one way of doing this using multiple error codes. But the real answer is that your problem is probably not "I needed to have an error of error codes". It's probably "I'm not doing anything to try to make my error indications meaningful." Which is what I said above.
What you're trying to say is that, instead of returning the constaint name as a string, the DBMS could assign it a 32-bit id, return this id as an error, then the ORM could proparage this 32-bit id, and then my application could somehow match that to figure out which DB validation failed? That's technically possible, but usually not worth the trouble, unless you're doing high-performance stuff.
None of this is important for high performance stuff since you only do errors on failure case.
I'm saying you're defining the problem wrong. You need to get to what the real problem is. The problem isn't you don't have enough error codes. The problem is that you are not making an effort to do anything useful to the customer with them.
The poster gave one example. The example is based upon an idea of only interpreting error codes at the highest level. He wants more nested data because he's not doing any interpretation until the top.
Okay, so another possibility would be to interpret the codes on your way out instead of waiting until the top. If this function sees a value X from below it then the reportable error is foo. Now that you've resolved the error higher code doesn't try to re-resolve it.
But that's just one example. It's not meant to be any more comprehensive than the example in the post. What it is meant to do is elucidate the point that the fix isn't to have an array of error codes because the problem isn't that you didn't have enough erorr codes.
The problem is that the program (programmer) has made the error of conflating error codes with actionable data to report. They just aren't.
Another example, how many times have you seen a program just bomb out and print an exception code and a nested call stack? I know I've seen it a lot. Especially in python. The programmer didn't really make any attempt to handle errors so the "handling" is to fly to the highest nested call (above "main") and what it does is just to blast out a bunch of stuff that only means something to the programmer.
Java sees this, sees it as a problem and so makes it illegal to call a function that might return an exception unless you have a handler. So then what? Programmers just fix the proximate problem and put in a rethrow or similar so they can get back to "the real coding" and move forward.
Programmers just typically handle errors poorly. We have myriad attempts to force them to do better, but you just really can't. Even having an array of error codes doesn't fix the root problem. To have good error handling you have to put in effort to have it. However you do it is certainly fine, but since the problem is with programmer not doing a good job there's no algorithmic fix like this post presents.
However you go about improving the issue of unactionable error messages to the user is fine by me. If this is part of your solution then fantastic.
nikita2206@reddit
Completely agreed, ideally you need to be able to define “child” error types that declare their own fields with extra data, allowing consumers to handle these errors in more ways than just rethrow them.
jcelerier@reddit
But then you throw performance out of the window if you have some hot loop that needs to do error handling and your error object doesn't fit in a register anymore
Expurple@reddit (OP)
As you should be, if your app is that performance-sensitive. You use the idiomatic/maintainable solution by default, and a more performant solution where it matters.
BenchEmbarrassed7316@reddit
I would prefer to have a smart compiler that understands how I use the function result and optimizes it. In that case, the function returns more detailed information, and the caller decides whether this information is needed and makes ignoring it zero cost.
ShinyHappyREM@reddit
Ah yes, a SufficientlySmartCompiler.
What about dynamically loaded code, or people who use a different compiler?
BenchEmbarrassed7316@reddit
C uses same "SufficientlySmartCompiler" to be fast:
C Is Not a Low-level Language https://queue.acm.org/detail.cfm?id=3212479
Moreover, we now have a Rust compiler that is so smart that it can automatically manage memory without GC.
So in this case, when I talk about "A smarter compiler" - it's much closer to reality.
Full-Spectral@reddit
I'm a huge Rust advocate, but it doesn't automatically manage memory without GC. It's RAII based. It's just that most of the time you don't need to deal with that yourself since the bulk of it (sometimes all of it in some systems) is handled by standard library types.
Expurple@reddit (OP)
I agree that we could have a more flexible and performant ABI for
Result. Seeiex, for example.ShinyHappyREM@reddit
Just make your "error object" a pointer that points to null if there's no error, and otherwise points to a union/struct/instance that holds the error info.
LIGHTNINGBOLT23@reddit
To add to this, it's also easy enough to pre-allocate all or most of an error object before it is ever returned, if the loop must remain as hot as possible. Maybe even make it
staticto avoid a heap allocation entirely and have a flag in the error object to indicate that it's not heap allocated.Iggyhopper@reddit
avx512 hot loop optimized error object allocation pools for increased error code output
Oh my I can't wait.
LIGHTNINGBOLT23@reddit
You could also keep circumstantial flat error codes, at least on x86, by using the non-canonical address range, provided that you never dereference the pointer. A sentinel-like value such as
#define SIZE_TOO_SMALL ((CleverError*)0xDEADDEADDEADDEADULL)would be sufficient. Portability and adherence to the C standard is not guaranteed. No refunds.Iggyhopper@reddit
I refuse to believe that anyone needs more than 4 million error codes.
Mognakor@reddit
What if i want non-fungible errors (NFEs) ?
ShinyHappyREM@reddit
As is tradition.
mort96@reddit
Heap allocated errors make this a non-issue, the error pointer still fits in a register so you can still do hot loops with error handling. The error check just needs to be an error check that'll be negative in roughly 100% of cases.
Of course hot loops with error handling where errors happen in the typical case would be problematic. But if your fast path includes an error being produced, you're doing it wrong.
simonask_@reddit
Lots of errors are good and expected. For example, validating untrusted input, which is most input, should fail in ways that ideally aren’t any slower or faster than the happy path.
The reason is that timing attacks exist. If an attacker can determine if they did something right, or triggered an exceptional error path, you are leaking information to them. In the best case, they can DoS you. In the worst case, all of your moneys is gone.
sigma914@reddit
Surely you can just use the top bits to tag it as a pointer or code and then box your error variant
inio@reddit
Look at the Absiel
Statustype.ShinyHappyREM@reddit
^^*Abseil
Pharisaeus@reddit
Did someone just "invent" exceptions, exceptions nesting and passing along the stack trace?
max123246@reddit
If exceptions were part of the function return interface and had nice syntax like
?to explicitly rethrow the error so that the user acknowledges it's a falliable function, I would love exceptions.Sadly most languages do neither
Mognakor@reddit
Sounds like Java's checked exceptions and people did not like them.
HappyAngrySquid@reddit
I wouldn’t mind them if they could be inferred rather than explicitly listed all over the place.
Expurple@reddit (OP)
Error codes < exceptions <
ResultPharisaeus@reddit
Yes and no. Optional/Either/Result/Monads have slightly different purpose. Even in Rust you have
panic;) Anyway, "discovering" in 2026 that error codes are not enough is just ridiculous. I mean it's a good observation, but it's also common knowledge for at least 40 years.Expurple@reddit (OP)
I was just as surprised that the 2026 advice to use error codes got as much traction as it did! My post is in response to that one.
I agree that
panicis a different use case. Result/Either is closer to checked exceptions specifically. And yes, it's strictly better than those. I go into detail in the link above.MonsieurCellophane@reddit
This made me instantly think of java & the endless stack traced it's wont to display at the drop of a hat. Antipodal to flat error codes and not pretty either.
Expurple@reddit (OP)
For the purpose of nesting errors and providing extra details for recovery or logging, Java exceptions are actually pretty great. Your issue is simply with the default display style. You can have a similar nightmare with nested error values in Rust, if you print them using the verbose
Debuginterface, instead of theDisplayinterface meant for the users.Linguistic-mystic@reddit
No they are not great. They don't provide any data from function args or local variables. Just because an exception was thrown somewhere in a loop doesn't tell you what was being processed on that iteration. And even if you include that info in the exception message, it's still not of much help because you also usually need that info on other stack frames. Which forces the developer to catch and rethrow everywhere, defeating the purpose of stack traces completely (what's the good of an automatic stack trace if one has to enrich that stack trace manually).
As a Java developer, I dislike stack traces with a passion because they are only useful in the most trivial cases. Any looped code and a stack trace doesn't do crap to help investigate.
KitAndKat@reddit
I've thought about the issue of error reporting in the past, but never implemented it. I think you need an Error object, which is a stack of errors, e.g.:
The user is told "could not complete transaction" with a Details button which shows the next level down. If you just pass the original error all the way up, the user is told "file read error", which is next to useless.
RedEyed__@reddit
Could be security issue: you can break system by analyzing error stacks
lelanthran@reddit
Only if you pass the entire stack to the user. You could log the entire stack and pass only the top two items to the user.
Besides, just how much of a security breach can it possibly be? I regularly see entire exception backtraces in C# WebApps and in almost every Java app too, but all the exploits we see don't appear to be as a result of those.
ChemicalRascal@reddit
If you punt a stack trace at me, and I can see from it that you're using a particular library to, for example, parse CSVs from user input, I can then tailor my attacks to focus on exploits in that library.
It's not necessarily knowing the stack itself that helps attackers. It's what you know when you know the stack that helps.
lelanthran@reddit
I don't disagree that it is a security issue, I just think that on a scale from 1-10, it's probably a 1.
After all, just knowing that the server is written in C# already narrows down your potential library targets to maybe 2 or 3 for each library (JSON parsing, or third-party requests, or ORM). Same for almost any other language. In some non-mainstream language, you can be sure of a single library being used (Haskell, Ocaml, etc).
Everything is a security issue, and it becomes a trade-off between having users submit an error message that requires no deep dive into your logs and having a malicious actor use that info to target your system.
ChemicalRascal@reddit
Right, but do you know which version is being used? There's a reason OWASP talks about this stuff. If you're doing something important, with other people's data, you can't afford to expose a bigger attack surface than you have to, and reducing the visibility of the attack surface helps.
If you want to access those the trace, great. Log it, give it a GUID, report the GUID to the user. Or a truncated code if you don't want to overwhelm them with a long string of characters. It's also a lot easier to copy that trace out of a database than it is a photograph of a screen covered in schmutz.
lelanthran@reddit
I agree; defense-in-depth must necessarily include obscurity!
This is only for the use-case where the user sends you the error message. I was including the use-case of "User examines the message and knows how to fix the problem".
For example if it is a permissions problem, they know they have to apply to their supervisor to be added to the special-people role.
If the error is "The image you attempted to link into your issue is not found:: ", then they know that someone deleted the image that was uploaded, or that they typo'ed the image name, etc.
That's why it's usually sufficient to give the user the top-level error that occurred (or maybe the highest two levels), along with the parameters that were used.
ChemicalRascal@reddit
Haaar haaar. There's a difference between obscurity and not showing your whole ass.
No, you can do this for everything. Every exception you catch, you can log it and display a GUID.
What you're describing here is business logic working normally. Totally different thing, you wouldn't use exceptions for that anyway.
Probably. Please tell me you're not using exceptions for authorisation checks. Please.
Please?
Uh. We're talking about stack traces. I'm not advocating against meaningful error reporting to users. I'm advocating for suppression of stack traces.
lelanthran@reddit
Okay, that works for exceptions, but the example I have (ACL failure) isn't an exception; what does a user do with a GUID then? Mail the admin, ask "what does this mean", and have the admin respond "It means you need to ask your supervisor to add you to the correct group".
Why would I? ACL failure doesn't result in an exception; that's an expected result, not an exception.
Are we? I said "give the user the error that occurred", not the source code filename, line number and "value is NULL" information. If the error being returned is "link reference does not exist", that line alone is sufficient. All the other stuff you store/log (or discard as you see fit).
ChemicalRascal@reddit
You don't use that method for that kind of error. You handle it properly. The whole point of this was stack traces getting through to the front end, which means errors aren't being handled properly.
Exactly. See, I was worried you were talking about using exceptions for authorisation failures because we were talking about exceptions and then you started talking about authorisation failures, which makes it sounds like you're using exceptions for authorisation failures, which is silly.
Business logic errors, including authorisation errors, should not be handled through exceptions, especially if they're user-facing. It's an entirely different type of failure condition. You handle those with properly handled feedback to the user, because you can, because you built the system to handle that scenario.
An uncaught exception is definitionally not be that you built the system to handle. So you can't actually know in advance the information you're about to show to the user. So you give them a generic error, a GUID, and log the details. That's the safe way to do that.
Yes.
Blue_Moon_Lake@reddit
Error stack is for debugging and logging, not recovering.
Blue_Moon_Lake@reddit
If you have a stack, you don't need all that nesting for debugging your code.
KitAndKat@reddit
Yeah, I was thinking about it from a user viewpoint.
Lord_Skellig@reddit
Certainly helps though
Expurple@reddit (OP)
Yeah, I think that's usually called "error chaining". I know a few Rust libraries that implement that. For example,
lazy_errorstrmetroidmaniac@reddit
Reinventing exceptions from first principles
Expurple@reddit (OP)
Error codes < exceptions <
Result😏trmetroidmaniac@reddit
Sure, but that's not the point of this article. The point of this article is that an error should be rich with nested data, not just an integral flag and maybe an error string.
This comes very naturally with exceptions (exception chaining) but can be done or not done with any one of these mechanisms. That overdebated bikeshed concerns control flow and syntax and is orthogonal to this particular issue.
max123246@reddit
My problem with exceptions is simply it's not part of the function return-signature. That's kind of it, to be honest.
maxinstuff@reddit
Agree - in languages that CAN throw, you basically have to be prepared for anything to throw at any time for any reason.
And it’s leaky AF - something unexpected happens and my app turns it’s call stack inside out and throws it at the caller like a stripper at a stag do.
DearChickPeas@reddit
Java's @ throws(Exception) everywhere really didn't make the code pretty.
trmetroidmaniac@reddit
I think type inference could go a long way to make checked exceptions less verbose.
DearChickPeas@reddit
I think that's what Kotlin did, I longer see exception annotations.
trmetroidmaniac@reddit
Kotlin straight up doesn't check exceptions.
ChemicalRascal@reddit
Right, but OP wrote this piece as a push-back against the notion of flat error codes. In the context of that discussion, you can't really leap from flat codes to exceptions without any sort of motivation in the middle.
To move people from flat error codes, to change minds, you kinda need to argue the case that points towards exceptions. It makes sense to go back to first principles here.
lelanthran@reddit
Depends on what you are using that error for. Error message for users should include an action that they can take to fix the error. Error messages for devs should include the inputs that resulted in the error.
Resultis useless is both those situations whileExceptionis useful in one of them.Flat error codes are of two types:
Descriptive error code
ENOENTorEEXISTwhich can be mapped to a descriptive string that doesn't contain the inputs that caused the error (for example, user seesfile not found, and not./some.file.txt: file not found)Non-descriptive error code, like
open()returning a negative number on failure - that number tells you nothing and you need to useerrnoto get the descriptive flat error code.The problem with error codes is not the code, but the fact that most in-house error codes don't map to a message, and even if you wanted to, you couldn't, because one library might return
20for "invalid input" error while another library may return20for "record doesn't exist" error. There is no global mapping under this scheme and so error codes break down.Either way,
Flat error codeis at the bottom of the usefulness stack, but it's not clear thatResultis at the top.Expurple@reddit (OP)
What makes you say that? In Rust, the error types stored in
Result::Errusually implement both Display and Debug traits, for those exact purposes.lelanthran@reddit
(Emphasis mine) My point is that Exceptions give you one of those for free. Using Error types requires discipline from the developer (implement the Error type traits for a specific error), and then more discipline to avoid a
panic. Flat error codes require even less code that using Error types, but requires even more discipline, discipline which the compiler cannot even catch.I think my broader point is that Error types (or error handling) is not all that important at the language level; you can do the correct thing in any language with a little extra work. I have a pretty complex system written in C. For errors I decided up-front how to handle them:
Errors are reported using a variable argument macro; this is pushed into a global thread-safe queue, including source code line number, source filename, values of the parameters to the failing function call, and any other arbitrary message I want to put into it. The function then returns a sentinel value indicating error (NULL, maybe, or similar).
As each function checks for error after calling any function, failures can stack - effectively building a stack trace that consists of all the inputs into the actual error.
At the top-level, if the top-level operation was initiated by the user, a set of user-facing error messages are displayed.
As the queue is routinely persisted to disk, it is truncated in RAM.
Now I don't really like the mechanics of this at the language level, but at the system level this is perfect - I don't mess my metrics up by mixing up errors and logging messages and the user gets measures to take instead of "Error 0x45929: Contact your administrator". There's no Error type so calling the macro with arbitrary arguments is less work than attempting to define an error for every little function that might return something.
The reason my approach works is because there is no "recovery" attempt; there's no point, actually. "Something went wrong" == "Discard that entire code path and do the next thing instead".
I also think that if you are taking different code paths based on an Error type, you have mixed up program logic and error handling logic; if the code is recovering from something, then that thing is not an error, it's part of the business logic (example: "recovering" from a failed HTTP request by retrying - that was not an error. It becomes an error only if the retries are exhausted. Or reading a file, but creating it if it doesn't exist - once again, that's not recovering from an error, that's just business logic).
In my mind, an error is a hard stop. It stops the program from proceeding. The only acceptable way to handle it is by unwinding the call stack to a safe point and recording that the attempt to do something failed (and why it failed). The only exemption I would make is if the safe point is the very top level of the program and the program is an interactive one with a user sitting in front of it - at that point you can ask the user what to do.
The whole reason for doing an Error taxonomy (checked exceptions, error types, etc) is to enable recovery. I prefer the Erlang approach - kill that task and move on.
Expurple@reddit (OP)
You need to manually write an impl (or use a macro like
#[derive(Debug)]orthiserror), but there's no discipline involved in that. The compiler will just tell you when you try to print a non-printable error or compose it with a supposedly-printable error.No different from avoiding an inaproppriate
abortor an uncaught unchecked exception in other popular languages.In theory, you can. In practice, you want. My post about exceptions links a recearch paper about broken error handling patterns that come up all the time in languages with exceptions.
I would still call that an error, but that's just a choice of words. I agree with your main idea: it's very useful to make a distinction between "expected"/recoverable errors and unexpected bugs / edge cases where it's accceptable to abort the entire task. My post about exceptions discusses this too.
I disagree. There are other benefits to having an explicit taxonomy. See "Why Use Structured Errors in Rust Applications?"
UselessOptions@reddit
Tell that to Zig devs, who insist on having functions return a single error code with no details whatsoever.
HolySpirit@reddit
The Zig way, as I understand it, is to pass a pointer to a
Diagnosticsstruct that is populated when an error happens. I think it fulfills what the author of the article wants, because you can simply have a field with the diagnostics struct from the lower-level library in the diagnostics struct of the higher-level library.I think you should be able to generate high-quality error messages from this kind of diagnostics structure plus the program state when the error occurred for context.
FWIW I don't have much hands-on experience with this approach since I didn't use Zig too much yet, except for some toy examples.
quetzalcoatl-pl@reddit
just make the integer type of the error code have enough bits
like, u1024 ( ͡° ͜ʖ ͡°)
trmetroidmaniac@reddit
With a pointer you can encode arbitrary data ( ͡° ͜ʖ ͡°)
wasabichicken@reddit
As a C programmer, I'm proud of y'all.
quetzalcoatl-pl@reddit
:manofculture.jpg" :)
max123246@reddit
Zig doesn't have a good library story considering comptime being something you can rely on across library boundaries with no way of knowing as the library owner that making it not comptime-compatible will break the code. If I were to reach for Zig, it'd be the same reasons I reach for C, I want build everything myself. Turns out, most of the time, I really don't want to do that, lol.
Philluminati@reddit
Not only is it useful to capture error codes, it's useful to be able to build a stack as well, e.g.
UnableToConfigureDatabase -> Missing Credentials File -> File not found.
If you return "FileNotFound" without saying which file it can be an issue, and likewise, saying "unable to configure database" doesn't explain which aspect also failed.
Expurple@reddit (OP)
Yeah. That's basically nested error values in Rust, or exception chaining/wrapping in languages with exceptions.
mareek@reddit
One piece of software that use flat error code pretty well is Oracle Database.
There are thousands of ORAXXXXX error codes, each one represent a well defined situation and you know what you have to check if you want to fix the problem.
So if you do it well, I think Flat error code are OK
P.S. It's probably the only positive thing I can say about Oracle Database though
Expurple@reddit (OP)
I agree, they are OK in some domains. My point is that limiting yourself to error codes is a bad univeral advise. The (common) use case in the article can't be solved using plain error codes.
stewi1014@reddit
Hard disagree.
There are three types of information accociated with an error. 1. Information for the program to switch control flow. 2. Information for the developer to debug the problem. 3. Information for the user so they can take action.
1 belongs in an error code return value. Typically, a boolean "ok" return is the best error code. 2 belongs behind development configuration, and you should typically have external tools such as the debugger for this. 3. Belongs in a dedicated logging method.
Anything more complex than a boolean is starting to be too complex, and is begging justification. Rarely is there a legitimate use case for more than two control flows following an error. Distinguishing between a transient and persistent error is an example of where three return values would be wanted.
Expurple@reddit (OP)
I agree that it's useful to distinguish these three categories of error data.
My point is that it's actually a common case. Using just a boolean or a plain error code for control flow is a bad universal advise. My use case is nothing special: displaying the user a specific message depending on which database constraint is violated. You can't solve that if your database+ORM provides you only a boolean or an error code. You need the name of the constraint as a string. Additional attached error data, used for control flow.
blobjim@reddit
In C or Vala (or other GObject-supporting languages) you can use GError for this. A couple years ago it also got the ability to have extended arbitrary data (but it looks like it's kinda clunky to use).
2rad0@reddit
I've settled on context specific typed enums as error codes.
sailing67@reddit
flat error codes always felt like a design smell to me. you end up with massive switch statements everywhere adn half the codes are never documented properly. ive been on teams where we used structured error objects with a type, message, and context fields — way more useful when debugging in prod. the real issue tho is that most devs dont think about error handling until something breaks in production and then it becomes a scramble to figure out what went wrong
happyscrappy@reddit
Nested error codes are not enough either.
The problem isn't a lack of fidelity. The issue is that there primarily only two uses of error codes. One to know if something worked. And a single bit can do that.
The other is to help the customer know how to rectify their problem. One error code typically is not enough for that. Multiple also is not enough for that.
Stop thinking of users as people debugging your code and think of them just as using your tool for its functionality. You have to be able to convert your error codes into actionable messages and do it reliably.
And really few programs spend any time on doing this properly. So it doesn't matter all that much how deep your error reporting is.
Find a useful message to send to your customer. And if you want to put something in a file that can be reported to help you debug your code then do that separately. You can put a stack trace in there if you want (and if it is deemed not a security/privacy violation to do so). But don't get an idea that the detailed debugging information is going to be of help to a customer in resolving their issue. It's a help for you (your support people) instead.
Expurple@reddit (OP)
Have you actually read the post? It explains why one bit is not always enough.
happyscrappy@reddit
I did read the post. And no, it didn't do that. It explains why one value (beyond one bit) is not enough. And it not enough because of their specific implementation, not anything else. To be fair, the article does say this is "for my use case".
All you have to do is preserve enough information to be able to create a useful message to give to the user. Even a mere 32-bit value gives you enough fidelity for 4 billion different error cases. 4 billion different root error cases. You don't actually have to go to multiple values to do this.
The post proposes one way of doing this using multiple error codes. But the real answer is that your problem is probably not "I needed to have an error of error codes". It's probably "I'm not doing anything to try to make my error indications meaningful." Which is what I said above.
Expurple@reddit (OP)
What you're trying to say is that, instead of returning the constaint name as a string, the DBMS could assign it a 32-bit id, return this id as an error, then the ORM could proparage this 32-bit id, and then my application could somehow match that to figure out which DB validation failed? That's technically possible, but usually not worth the trouble, unless you're doing high-performance stuff.
happyscrappy@reddit
None of this is important for high performance stuff since you only do errors on failure case.
I'm saying you're defining the problem wrong. You need to get to what the real problem is. The problem isn't you don't have enough error codes. The problem is that you are not making an effort to do anything useful to the customer with them.
The poster gave one example. The example is based upon an idea of only interpreting error codes at the highest level. He wants more nested data because he's not doing any interpretation until the top.
Okay, so another possibility would be to interpret the codes on your way out instead of waiting until the top. If this function sees a value X from below it then the reportable error is foo. Now that you've resolved the error higher code doesn't try to re-resolve it.
But that's just one example. It's not meant to be any more comprehensive than the example in the post. What it is meant to do is elucidate the point that the fix isn't to have an array of error codes because the problem isn't that you didn't have enough erorr codes.
The problem is that the program (programmer) has made the error of conflating error codes with actionable data to report. They just aren't.
Another example, how many times have you seen a program just bomb out and print an exception code and a nested call stack? I know I've seen it a lot. Especially in python. The programmer didn't really make any attempt to handle errors so the "handling" is to fly to the highest nested call (above "main") and what it does is just to blast out a bunch of stuff that only means something to the programmer.
Java sees this, sees it as a problem and so makes it illegal to call a function that might return an exception unless you have a handler. So then what? Programmers just fix the proximate problem and put in a rethrow or similar so they can get back to "the real coding" and move forward.
Programmers just typically handle errors poorly. We have myriad attempts to force them to do better, but you just really can't. Even having an array of error codes doesn't fix the root problem. To have good error handling you have to put in effort to have it. However you do it is certainly fine, but since the problem is with programmer not doing a good job there's no algorithmic fix like this post presents.
However you go about improving the issue of unactionable error messages to the user is fine by me. If this is part of your solution then fantastic.
SiegeAe@reddit
Why nest when you could just append a message on handover?
As a consumer I typically only care about the error type I'm receiving and that the message includes enough relevant data.
Expurple@reddit (OP)
Have you read the post?
Impressive_Show_5552@reddit
guys play this game livewebtennis.com
sean_hash@reddit
Child error types with extra fields is orthogonal to ORM choice, it is about giving handlers more than a flat code to branch on.
Expurple@reddit (OP)
Like... yeah? That's what the post is about. It's about branching on a machine-readable DB constraint name. Not about my ORM choice.
bobody_biznuz@reddit
Sounds more like you have an issue with the ORM you're using?
Expurple@reddit (OP)
What? What issue are you talking about?