Handle errors with union
Asked Answered
A

1

7

I came to C from high-level Scala language and came up with a question. In Scala we usually handle error/exceptional conditions using Either which looks like the following:

sealed abstract class Either[+A, +B] extends Product with Serializable 

So roughly speaking it presents a sum of types A and B. Either can contain only one instance (A either B) at any given time. By convention A is used for errors, B for the actual value.

It looks very similar to union, but since I'm very new to C I'm not sure if it is sort of conventional to make use of unions for error handling.

I'm inclined to do something like the following to handle the open file descriptor error:

enum type{
    left,
    right
};

union file_descriptor{
    const char* error_message;
    int file_descriptor;
};

struct either {
    const enum type type;
    const union file_descriptor fd;
};

struct either opened_file;
int fd = 1;
if(fd == -1){
    struct either tmp = {.type = left, .fd = {.error_message = "Unable to open file descriptor. Reason: File not found"}};
    memcpy(&opened_file, &tmp, sizeof(tmp));
} else {
    struct either tmp = {.type = right, .fd = {.file_descriptor = fd}};
    memcpy(&opened_file, &tmp, sizeof(tmp));
}

But I'm not sure if this is the conventional C way.

Airfield answered 26/12, 2018 at 10:47 Comment(5)
It seem you should be using structures instead of unions. In a C union, only one single member can be "active". And because C and Scala are so different, you might want to reconsider your design, because one design doesn't really fit all languages.Topminnow
@Someprogrammerdude In a C union, only one single member can be "active". That's why I'm asking... For this particular circumstance file descriptor either valid (in this case I will make the int field active) or invalid (in this case I will make error_message active). Does it look weird in C?Airfield
Are you asking about using a tag to tell how a union shall be used? If so then "yes, that's conventional"Cote
@Cote Well, I will be relying on the const enum type type value to tell if the union contains a valid file descriptor or an error message string.Airfield
Maybe this is relevant for you en.wikipedia.org/wiki/Tagged_unionCote
E
6

I'm not sure if it is sort of conventional to make use of union for error handling.

No, it is not. I would heavily discourage against it, because as you can see, it generates a lot of code for something that should be really simple.

There are several much more common patterns. When the function operates on a structure, it is much more common to use

int operation(struct something *reference, ...);

which takes a pointer to the structure to be operated on, and returns 0 if success, and an error code otherwise (or -1 with errno set to indicate the error).

If the function returns a pointer, or you need an interface to report complex errors, you can use a structure to describe your errors, and have the operations take an extra pointer to such a structure:

typedef struct {
    int         errnum;
    const char *errmsg;
} errordesc;

struct foo *operation(..., errordesc *err);

Typically, the operation only modifies the error structure when an error does occur; it does not clear it. This allows you to easily "propagate" errors across multiple levels of function calls to the original caller, although the original caller must clear the error structure first.

You'll find that one of these approaches will map to whatever other language you wish to create bindings for quite nicely.


OP posed several followup questions in the comment chain that I believe are useful for other programmers (especially those writing bindings for routines in different programming languages), so I think a bit of elaboration on the practical handling of errors in in order.

First thing to realize regarding errors is that in practice, we divide them into two categories: recoverable, and unrecoverable:

  • Recoverable errors are those that can be ignored (or worked around).

    For example, if you have a graphical user interface, or a game, and an error occurs when you try to play an audio event (say, completion "ping!"), that obviously should not cause the entire application to abort.

  • Unrecoverable errors are those serious enough to warrant the application (or per-client thread in a service daemon) to exit.

    For example, if you have a graphical user interface or a game, and it runs out of memory while constructing the initial window/screen, there is not much else it can sanely do but abort and log an error.

Unfortunately, the functions themselves usually cannot differentiate between the two: it is up to the caller to make the decision.

Therefore, the primary purpose of an error indicator is to provide enough information to the caller to make that decision.

A secondary purpose is to provide enough information to the human user (and developers), to make the determination whether the error is a software issue (a bug in the code itself), indicative of a hardware problem, or something else.

For example, when using POSIX low-level I/O (read(), write()), the functions can be interrupted by the delivery of a signal to a signal handler installed without the SA_RESTART flag using that particular thread. In that case, the function will return a short count (less than requested data read/written), or -1 with errno == EINTR.

In most cases, that EINTR error can be safely ignored, and the read()/write() call repeated. However, the easiest way to implement an I/O timeout in POSIX C is by using exactly that kind of a interrupt. So, if we write an I/O operation that ignores EINTR, it will not be affected by the typical timeout implementation; it will block or repeat forever, until it actually succeeds or fails. Again, the function itself cannot know whether EINTR errors should be ignored or not; it is something only the caller knows.

In practice, Linux errno or POSIX errno values cover the vast majority of practical needs. (This is not a coincidence; this set covers the errors that can occur with a POSIX.1-capable standard C library functions.)

In some cases, a custom error code, or a "subtype" identifier is useful. Instead of just EDOM for all mathematical errors, a linear algebra math library could have subtype numbers for errors like matrix dimensions being unsuitable for matrix-matrix multiplication, and so on.

For human debugging needs, the file name, function name, and line number of the code that encountered the error would be very useful. Fortunately, these are provided as __FILE__, __func__, and __LINE__, respectively.

This means that a structure similar to

typedef struct {
    const char   *file;
    const char   *func;
    unsigned int  line;
    int           errnum;  /* errno constant */
    unsigned int  suberr;  /* subtype of errno, custom */
} errordesc;
#define  ERRORDESC_INIT  { NULL, NULL, 0, 0, 0 }

should cover the needs I personally can envision.

I personally do not care about the entire error trace, because in my experience, everything can be traced back to the initial error. (In other words, when something goes b0rk, a lot of other stuff tends to go b0rk too, with only the root b0rk being relevant. Others may disagree, but in my experience, the cases when the entire trace is necessary is best catered by proper debugging tools, like stack traces and core dumps.)

Let's say we implement a file open -like function (perhaps overloaded, so that it can not only read local files, but full URLs?), that takes a errordesc *err parameter, initialized to ERRORDESC_INIT by caller (so the pointers are NULL, line number is zero, and error numbers are zero). In the case of a standard library function failing (thus errno is set), it would register the error thus:

        if (err && !err->errnum) {
            err->file = __FILE__;
            err->func = __func__;
            err->line = __LINE__;
            err->errnum = errno;
            err->suberr = /* error subtype number, or 0 */;
        }
        return (something that is not a valid return value);

Note how that stanza allows a caller to pass NULL if it really does not care about the error at all. (I am of the opinion that functions should make it easy for programmers to handle errors, but not try to enforce it: stupid programmers are more stupid than I can imagine, and will just do something even more stupid if I try to force them to do it in a less stupid fashion. Teaching rocks to jump is more rewarding, really.)

Also, if the error structure is already populated (here, I am using errnum field as the key; it is zero only if the entire structure is in "no error" state), it is important to not overwrite the existing error description. This ensures that a complex operation that spans several function calls, can use a single such error structure, and retain the root cause only.

For programmer neatness, you can even write a preprocessor macro,

#define  ERRORDESC_SET(ptr, errnum_, suberr_)       \
            do {                                    \
                errordesc *const  ptr_ = (ptr);     \
                const int         err_ = (errnum_); \
                const int         sub_ = (suberr_); \
                if (ptr_ && !ptr_->errnum) {        \
                    ptr_->file = __FILE__;          \
                    ptr_->func = __func__;          \
                    ptr_->line = __LINE__;          \
                    ptr_->errnum = err_;            \
                    ptr_->suberr = sub_;            \
                }                                   \
            } while(0)

so that in case of an error, a function that takes a parameter errordesc *err, needs just one line, ERRORDESC_SET(err, errno, 0); (replacing 0 by the suitable sub-error number), that takes care of updating the error structure. (It is written to behave exactly like a function call, so it should not have any surprising behaviour, even if it is a preprocessor macro.)

Of course, it also makes sense to implement a function that can report such errors to a specified stream, typically stderr:

void errordesc_report(errordesc *err, FILE *to)
{
    if (err && err->errnum && to) {
        if (err->suberr)
            fprintf(to, "%s: line %u: %s(): %s (%d).\n",
                err->file, err->line, err->func,
                strerror(err->errnum), err->suberr);
        else
            fprintf(to, "%s: line %u: %s(): %s.\n",
                err->file, err->line, err->func, strerror(err->errnum));
    }
}

which produces error reports like foo.c: line 55: my_malloc(): Cannot allocate memory.

Eighteenmo answered 26/12, 2018 at 11:9 Comment(13)
These approaches have the disadvantage that it's easy to forget to check the error code, silently ignoring the error, whereas a union-based approach forces you to check.Northeast
@BrianMcCutchon: Does it (force you to check)? No, I don't think so. Developers who assume success will simply assume success either way. Instead of checking the tag (identifying which member of the union is valid), they'll just use the member of the union directly.Eighteenmo
@BrianMcCutchon: Perhaps it is better to think of it this way: C does nothing to stop you from shooting yourself in the butt. It may sound harsh and unfriendly, but it turns out to be one of its major strengths. (It also means that buffer overruns and off-by-one errors are more prevalent than in other programming languages. On the other hand, it keeps the runtime light, and makes it easy and simple to interface to other programming languages.) The responsibility to write sensible code is completely upon the human programmer. I advocate making that easy/easier, but not enforcing it.Eighteenmo
I have a question about object allocation. Suppose I defined opaque struct my_struct_t and a function for its initialization/releasing struct my_struct_t *initialize(int);, int release(struct my_struct_t*);. Is it a common/good practice to return a NULL pointer in case initialization failed for some reason? In case of trying to release NULL -- to return an appropriate error code?Airfield
@SomeName: Returning NULL in case of an error is expected; see e.g. malloc(), calloc(), realloc(). Releasing/freeing NULL is usually safe and a no-op; see e.g. free(). In particular, free(NULL) is safe and does nothing.Eighteenmo
@SomeName: However, I would advise against an opaque structure, and instead use an initializer macro, MY_STRUCT_INITIALIZER, so that an error structure can be declared and initialized in one line using struct my_struct foo = MY_STRUCT_INITIALIZER;. This is used in e.g. pthread mutexes and condition variables (PTHREAD_MUTEX_INITIALIZER and PTHREAD_COND_INITIALIZER), and it does make the code simpler and shorter to write.Eighteenmo
@NominalAnimal You mean sort of initialiser macro function with a parameter (int in my case)? Actually in my case the only intention to use the opaque struct was to abstract over OS-specific details. So I can build a static library for each OS with its own definition of struct my_struct. Is it abusing of opaque structs?Airfield
@SomeName: No, I meant for those using your library to initialize the error structure to a "no error" state. You can hide the OS-specific details in the header file, with #if defined(_WIN32) .. #elif defined(__linux__) .. #elif defined(__APPLE__) && defined(__MACH__) .. #elif defined(__FreeBSD__) .. #else #error Unsupported operating system! #endif or similar, so the same header file is used on all OSes; see Pre-defined Compiler Macros for details.Eighteenmo
It's interesting about the typedef struct errordesc that you mentioned. Would it look natural if we have a linked list of such errordescs instead so we can have the whole error cause trace. Or this is not really common to do in C?Airfield
@SomeName: I would recommend against a linked list, because you might fail to report an error if there is a problem with memory management/allocation. I added a section about error reporting and management that hopefully clears some of the questions you posed; do let me know if you need more detailed advice. I do believe this is useful for others pondering error handling, and that we can do better than is currently typically done, so this is an important/useful/practical topic to work on.Eighteenmo
@NominalAnimal I have one more question regarding struct errordesc. Would it look common to make an error parameter type to be struct errordesc **? In such a case we could pass a pointer to a null pointer initially and in case of error condition we can assign a pointer to an actual error. Why I think it can be useful is 1. Error can be easily propagated down the call stack (we have struct errordesc **). 2. We can make struct errordesc immutable.Airfield
@SomeName: Not common, but not odd either; more like "oh okay, there must be a good reason for doing it like that". If you really need error stacks like that, then yes, that is how you'd do it. (Put the struct errordesc *next as the first element in the structure.) However, like I wrote above, I have never found error stacks to be practical, as only the root error tends to be significant. (When one thing goes wrong, things tend to avalance from there; with only the root cause being important.) I do not see any extra value in having the structure immutable.Eighteenmo
@SomeName: Consider Java stack traces as an example. They are often several thousand characters long. For what? Usually, the problem is obvious when you look at the root problem: something was a null reference, or an out-of-bound index, and to solve the bug, you think how that could happen. The rest of the stack trace is basically useless. In those rare cases where you do need a stack trace, you can run the executable in a debugger, duplicate the error and get a core dump (snapshot of the process at the point of error), and examine the entire state till your heart is content.Eighteenmo

© 2022 - 2024 — McMap. All rights reserved.