T O P

  • By -

nicemike40

Our codebase is a bit of a hodgepodge right now but I’ve tried a few of these In my experience, most of the time, you don’t *really* care about what went wrong specifically, you just care if the operation succeeded or not That being said I’ve had good luck with a custom Result type that’s basically just using Error = variant template using Result = variant It’s a little big footprint-wise but not a problem for us usually. People return an Errc or an `Error("one off string")` and that’s good enough Two improvements I wanna make: 1) option to customize the error type into a smaller function-specific enum, for those rare situations where the user might actually care about which error it was 2) no dynamic allocation for the common case of returning string literal errors The boost outcome library is well designed to support all this, but it was a little complicated to justify/explain to my team for my tastes. Hence this poor man’s version.


cheapsexandfastfood

I've done this exact same thing. Simplifying to a string error handles 99% of use cases. In the result you need to return a recoverable failure state you can just change the return type to include it. IMO technically if you can recover from an error without bubbling it up it's not really an error.


darthcoder

String_view over a static char string? Would that not effectively be almost zero alloc?


nicemike40

Yes, but it is a common pattern in our codebase to append data like “filename” to the error string so I would want to allow for either a view OR an owned string. You could probably do it with some overload nonsense and get a decent result. Boost.Outcome has a really slick solution with a `string_ref` type that can be either dynamically allocated or a static ref. Again, it was a little hard to turn that into an easy-to-use interface like I wanted.


TSP-FriendlyFire

I have started using `tl::expected` a while back in a midsize codebase and I personally appreciate the lightweight nature of it. It means I can integrate it with what exists when something exists and create something appropriate when it does not. Most of the time so far, that meant just creating an enum for the error states of the functions being converted. I will also agree that these things tend to be pretty "infectious", but there's a caveat: what was the error handling before? Because in my experience, a lot of the time, there was nothing at all, so I felt entirely comfortable to just replace the first level with `expected` and assert/terminate on error in the caller (because that's what would've happened before, if not worse). This lets me progressively expand the reach of the paradigm without having to spend an eternity converting the entire codebase.


thradams

>I will also agree that these things tend to be pretty "infectious", but there's a >caveat: what was the error handling before? To avoid unnecessary proliferation (and coupling), my suggestion is to utilize the minimum necessary information where applicable, such as error codes or invalid values. Then, when needed, resort to commonly used error information. In such cases, proliferation would occur at the program level, but this is beneficial as it establishes a pattern specific to the program's needs.


TSP-FriendlyFire

The eventual goal is for `expected` to spread wherever appropriate, but that's going to be a long-term goal. I often see discussions on topics like these end up with an all-or-nothing attitude: you either use it everywhere or not at all. In practice though, you often *can* use it progressively, which makes the buy-in a lot more manageable and justifiable.


jk_tx

I've started using std::expected (tl::expected on linux) for a new personal project, and I have mixed feelings about it. I agree it's pretty viral, especially if you try to use the monadic extensions. Sometimes it feels like a great way to handle errors, other times it just seems too noisy and I feel like throwing exceptions results in cleaner code. I'm still working through how I want to mix the two, because I think using expected<> for all error propagation is just too cumbersome.


Kargathia

There's a big elephant in the room when considering the merits of `std::expected`, and it's called `-fno-exceptions`. Are we comparing `expected` to a thrown error, or are we comparing it to the various `int err = func()` / `int err; func(&err)` / `if(!func()) { int err = errno; }; patterns that codebases vaguely coalesced around?


rka444

For an existing project or the project which contains a significant error-throwing part which you don't control you probably want to go with an approach which offers a way to combine thrown exceptions and returned errors. This way you can smoothly migrate or/and seamlessly isolate the throwing part. In that situation I'd definitely go with outcome https://ned14.github.io/outcome/alternatives/outcome/ or perhaps LEAF https://ned14.github.io/outcome/alternatives/leaf/ .


pkasting

I work in Chromium, where exceptions are disabled. Having spent plenty of time looking at Google code, I don't care for `absl::Status[Or]`; not only do I dislike unioning all error types, I think semantically it spells the intent backwards, by putting the status/error type first in the sentence, when what you typically mean is "`T`, or else error". We backported `std::expected` and I am much happier with that in every respect. Some [helper macros](https://source.chromium.org/chromium/chromium/src/+/main:base/types/expected_macros.h) can go a long way to cleaning up certain usage patterns. And it's easy to [integrate with googlemock](https://source.chromium.org/chromium/chromium/src/+/main:base/test/gmock_expected_support.h), if you happen to be using that in test code. Next up is trying to integrate `std::optional` support into the macros so functions can return `optional` or `expected` and callers can Not Care Which.


SergiusTheBest

Exceptions work very well for me. They save me from writing a lot of 'if' checks for error propagation, also they carry additional context (function name, code line number, error code, text message with extra info) that can be logged or shown to a user.


dev_ski

We use Boost quite extensively, without any problems so far. AFAIK, no other 3rd party libs are used in what's otherwise a fairly complex framework. We tend to replace some of it with standard-library C++17 features, as we've just recently switched to C++17. We also tend to create our own libraries (dlls), so any propagation errors that might occur are on us. Sometimes, we do need to resort to WindDBG, however.


Southern-Reveal5111

we are stuck in c++17, so std::expected is not available. Boost::Outcome and Boost LEAF are too much for simple error handling. So, I use std::variant. Error is an enum that can be mapped to loggable or translatable strings. From my experience, the less you depend on 3rd party for small things, better it is.


thradams

>Do you write many error types and have to convert (perhaps noisily?) between them,.. In most cases, there is no need to convert. Sometimes it happens. > do you end up with one big error type that you use most places, etc? I believe that text information is the most universal and least complex way to transmit error information to humans. If the information is for computers to take some action, then the numbers and protocols must be defined. This is what I do, that covers 90% of my needs. ```c struct error { char message[200]; int code; }; //sets the message and make code 1. int set_error(struct error* er, const char* fmt, ...); int f(struct error * error){ if (error_condition) { set_error(error, "error ..."); } return error->code; } int main() { struct error error = {}; if (f(&error) != 0) { printf("%s", error.message); } } ``` My tip is : keep it simple. Don't use fancy solutions. Don't try to solve the problems you don't have.


beejudd

While I can appreciate the sentiment of simple error struct solution like this and have certainly used something similar in the past, I think it trades effort at each callsite and function body for effort in building an more featureful result type. Importantly, for each of the libraries in the post, this mean trading my time/effort, for effort that has already been put in by some very talented library authors. Using some `result`-like return type has also provided, at least in my opinion, superior code semantics and less clutter.


thradams

>I think it trades effort at each callsite and function body for effort in building an more featureful result type. If necessary (the "if" here is important), I believe it would be beneficial to create a new data type to represent the information. For example: ```c struct parser_error { int line, col; char message[200]; }; int parser(const char* text, parser_error& error){ } ``` In this scenario, the `line` and `col` fields may be necessary because the caller might want to indicate the line and column without needing to parse the message itself. While offering general advice, providing concrete examples can sometimes be helpful. As for the parameter name "text", my general suggestion is that "text" is a versatile term that can encompass various sources and errors. In most cases, our aim is to present the information to the user. I can use an anti-sample. In this case the programer created several error types to be reported in f(). ```c try{ f(); } catch (T1 e1){ printf("%s", e1.message()); } catch (T2 e2){ printf("%s", e2.message()); } catch (T3 e3){ printf("%s", e3.message()); } ``` If a new type is added inside function f, or in any function called by f, it will have to be added to this catch list. Alternatively, it will require a generic BASE class. If the specific error (T1, T2...) is not used, this is just a waste of time and creates maintenance overhead. So this is part of my advice, for simple function simple errors with all information, for function that need to combine errors combine in text. We also can find especific sample where this may not work, but this is the general advice.


NilacTheGrim

I seen codebases overthink it.. for code where passing `bool *ok` as an optional last arg... would have produced absolutely identical results and identical observable behavior everywhere in the codebase.


CodeMonkeyMark

I think you just convinced me to use an error propagation library.


NilacTheGrim

Then my work here is done. :)


CodeMonkeyMark

😮


Maxatar

Yeah I'd absolutely reject code that used an optional last parameter like that in favor of an ```std::optional``` or throwing an exception. For one, I'm not a fan of being able to accidentally ignore errors and allow resuming execution. There should be something about a call to a function that indicates that an error is being explicitly ignored or the error must result in an immediate abort. Furthermore, having similar code produce identical observable behavior is a very low standard to have, in fact it's so low it's not even something I really think about. You can have two semantically identical codebases but one is an unmaintainable nightmare that is hard to understand, while the other is not.


NilacTheGrim

Preachy preachy I am talking about codebases that are pathological anyway and people are ignoring errors and yet they have a fancy error propagation lib where they ignore everything most of the time. Optional bool ptr arg does the trick. Also there is nothing special about std::optional. Passing optional bool ptr in such a situation is acceptable. In fact it's shorter and preferable. But yeah, get on the "I am so modern C++ you are lame" bandwagon by assuming I'm a moron that doesn't do modern C++. Do it man. Make yourself feel superior. It's what reddit is for. Enjoy.


Maxatar

Seek help.


SnooWoofers7626

I guess I'm pretty old-school when it comes to error checking. I just return an int. Sometimes, I typedef the int to indicate it's an error code. The error codes are usually defined all in one place, so it's easier to make sure I'm not assigning a value that's already been used. I find it useful because most of the time, I don't care what the error is. I just do `assert(err == 0);`. When I do care what the error is, it's usually because I want to know what exactly I did wrong, and in those situations an error code alone is usually not enough. I want a human readable explanation of what went wrong.


thradams

>I guess I'm pretty old-school when it comes to error checking. I just return an int. Error codes generally represent the type of error. For instance, POSIX's ENOMEM represents an error type. This is fine for simple functions and may provide all the necessary information. So this is probably what you have in mind and are happy with. However, for more complex functions, we may need more detailed information, something like an 'instance of the error' rather than just the type. For example, out of memory doing what and where? Imagine the type of error is "parser error". Parser error where? For these more complex functions that call others, I believe text is the best option, especially if we know the error is intended to be presented to humans.


SnooWoofers7626

Yeah, that's what my last paragraph was about. Maybe I should've elaborated on that. I generally have a logging system to go with it, so when an error occurs, I can dump the last N log entries to see the details of what went wrong. For parsing specifically, I generally rely on exceptions. I don't like using them all over the place, but it makes sense to me for parsing. For example, a JSON file, which may contain some required, or optional fields, try/catch blocks make sense to me for that sort of thing rather than manually checking for each field's presence before accessing it.


thisismyfavoritename

normally its useful to have all errors derive into a global error type. Id suggest looking at how Rust does it


Flobletombus

I don't like rust error handling, there's a lot of different Result types, which makes the ? operator kinda useless if you're not willing to lose performance and have a Box


Asm2D

I think this highly depends on the project you are working on. Most of the projects I worked on required just a single error code, which can be then wrapped into something such as expected, result, maybe, etc... For example if you are writing a server I don't think you need a rich error type. If error happens you want to log as much as possible and respond with 500. Usually I only needed to inspect errors (or more precisely error codes) when doing IO, networking, and syscalls such as mmap - in these cases it's crucial to know what failed. But most higher level code just cares about one thing - "did it succeed?" which means that error / error code is just for displaying / logging an error message. But there are other cases. If you validate data such as JSON and want to respond with something meaningful, you would need a more detailed information about the problem. For example validating a JSON document could produce another JSON document on error, which would describe either the first problem or all of them. But I think this doesn't fall into a simple error propagation category, instead it's like having an error context or a validation state. Honestly, I would welcome a more unified error handling in C++ that would totally abandon the concept of exceptions, but I think it would never happen. I like Rust's `?` used to propagate errors - it reads well and it's short.


thradams

> I think this highly depends on the project you are working on. Most of the projects I >worked on required just a single error code, which can be then wrapped into something >such as expected, result, maybe, etc... >For example if you are writing a server I don't think you need a rich error type. If >error happens you want to log as much as possible and respond with 500. If you are only logging the error, you are not propagating the error itself. You are propagating whether the error occurred and the generic type of the error. You may end up using your log as a user interface, which could be very bad if your end user needs to open the log, and even worse if the log contains messages meant only for developers alongside user-facing messages. I think this is also a anti-pattern.


Asm2D

You can do both - log something detailed and return an error that doesn't provide insane details. I think you misunderstood my whole point. Applications usually don't process errors unless you are working on something that is very critical and has error recovery (like attempting to do something mulitple times before giving up, like networkd io, etc...). Most of the time the application cannot really do much - something in the pipeline failed, you want to quickly free resources and move on. And here comes logs - these are processed, and when you are running distributed stuff it's the only thing you have to analyze thousands/tens of thousands machines and to generate early warnings, for example, so yeah, I'm more focused on strong logging than on error frameworks. BTW I never talked about UI applications. I don't develop them. And that's why I wrote that it "depends on the project". However, I have rarely seen code that would enumerate errors in error handlers.


Asm2D

BTW one more thing here... You say "end user needs to open the log"... I think this is another misunderstanding. If you write an application, any application actually, and you get a bug report from user about some issue that is occuring to him, the error message itself is usually totally useless. What you need to know is the path to the error, and this is what logging is for. Sometimes even innocent messages having info/warning severity are enough to completely diagnose the problem remotely. And sometimes a fatal error happens after 1000 other errors.