Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Handle errors with union

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.

like image 630
Some Name Avatar asked Dec 26 '18 10:12

Some Name


People also ask

How do you handle error handling?

You can propagate the error from a function to the code that calls that function, handle the error using a do - catch statement, handle the error as an optional value, or assert that the error will not occur.

How does GraphQL server handle failure?

If the request fails or partially fails (e.g. because the user requesting the data doesn't have the right access permissions), a second root field called "errors" is added to the response: { "data": { ... }, "errors": [ ... ] } For more details, you can refer to the GraphQL specification.

How do you simulate a GraphQL error?

To simulate GraphQL errors, simply define errors along with any data in your result. const dogMock = { // ... result: { errors: [{ message: "Error!" }], }, };


1 Answers

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.

like image 73
Nominal Animal Avatar answered Sep 20 '22 17:09

Nominal Animal