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.
int
field active) or invalid (in this case I will makeerror_message
active). Does it look weird in C? – Airfieldconst enum type type
value to tell if the union contains a valid file descriptor or an error message string. – Airfield