Error handling in C code

2019-01-01 16:41发布

What do you consider "best practice" when it comes to error handling errors in a consistent way in a C library.

There are two ways I've been thinking of:

Always return error code. A typical function would look like this:

MYAPI_ERROR getObjectSize(MYAPIHandle h, int* returnedSize);

The always provide an error pointer approach:

int getObjectSize(MYAPIHandle h, MYAPI_ERROR* returnedError);

When using the first approach it's possible to write code like this where the error handling check is directly placed on the function call:

int size;
if(getObjectSize(h, &size) != MYAPI_SUCCESS) {
  // Error handling
}

Which looks better than the error handling code here.

MYAPIError error;
int size;
size = getObjectSize(h, &error);
if(error != MYAPI_SUCCESS) {
    // Error handling
}

However, I think using the return value for returning data makes the code more readable, It's obvious that something was written to the size variable in the second example.

Do you have any ideas on why I should prefer any of those approaches or perhaps mix them or use something else? I'm not a fan of global error states since it tends to make multi threaded use of the library way more painful.

EDIT: C++ specific ideas on this would also be interesting to hear about as long as they are not involving exceptions since it's not an option for me at the moment...

21条回答
永恒的永恒
2楼-- · 2019-01-01 17:14

Returning error code is the usual approach for error handling in C.

But recently we experimented with the outgoing error pointer approach as well.

It has some advantages over the return value approach:

  • You can use the return value for more meaningful purposes.

  • Having to write out that error parameter reminds you to handle the error or propagate it. (You never forget checking the return value of fclose, don't you?)

  • If you use an error pointer, you can pass it down as you call functions. If any of the functions set it, the value won't get lost.

  • By setting a data breakpoint on the error variable, you can catch where does the error occurred first. By setting a conditional breakpoint you can catch specific errors too.

  • It makes it easier to automatize the check whether you handle all errors. The code convention may force you to call your error pointer as err and it must be the last argument. So the script can match the string err); then check if it's followed by if (*err. Actually in practice we made a macro called CER (check err return) and CEG (check err goto). So you don't need to type it out always when we just want to return on error, and can reduce the visual clutter.

Not all functions in our code has this outgoing parameter though. This outgoing parameter thing are used for cases where you would normally throw an exception.

查看更多
爱死公子算了
3楼-- · 2019-01-01 17:14

I ran into this Q&A a number of times, and wanted to contribute a more comprehensive answer. I think the best way to think about this is how to return errors to caller, and what you return.

How

There are 3 ways to return information from a function:

  1. Return Value
  2. Out Argument(s)
  3. Out of Band, that includes non-local goto (setjmp/longjmp), file or global scoped variables, file system etc.

Return Value

You can only return value is a single object, however, it can be an arbitrary complex. Here is an example of an error returning function:

  enum error hold_my_beer();

One benefit of return values is that it allows chaining of calls for less intrusive error handling:

  !hold_my_beer() &&
  !hold_my_cigarette() &&
  !hold_my_pants() ||
  abort();

This not just about readability, but may also allow processing an array of such function pointers in a uniform way.

Out Argument(s)

You can return more via more than one object via arguments, but best practice does suggest to keep the total number of arguments low (say, <=4):

void look_ma(enum error *e, char *what_broke);

enum error e;
look_ma(e);
if(e == FURNITURE) {
  reorder(what_broke);
} else if(e == SELF) {
  tell_doctor(what_broke);
}

Out of Band

With setjmp() you define a place and how you want to handle an int value, and you transfer control to that location via a longjmp(). See Practical usage of setjmp and longjmp in C.

What

  1. Indicator
  2. Code
  3. Object
  4. Callback

Indicator

An error indicator only tells you that there is a problem but nothing about the nature of said problem:

struct foo *f = foo_init();
if(!f) {
  /// handle the absence of foo
}

This is the least powerful way for a function to communicate error state, however, perfect if caller cannot respond to the error in a graduated manner anyways.

Code

An error code tells the caller about the nature of the problem, and may allow for a suitable response (from the above). It can be return value, or like the look_ma() example above an error argument.

Object

With an error object, the caller can be informed about arbitrary complicated issues. For example, an error code and a suitable human readable message. It can also inform the caller that multiple things went wrong, or an error per item when processing a collection:

struct collection friends;
enum error *e = malloc(c.size * sizeof(enum error));
...
ask_for_favor(friends, reason);
for(int i = 0; i < c.size; i++) {
   if(reason[i] == NOT_FOUND) find(friends[i]);
}

Instead of pre-allocating the error array, you can also (re)allocate it dynamically as needed of course.

Callback

Callback is the most powerful way to handle errors, as you can tell the function what behavior you would like to see happen when something goes wrong. A callback argument can be added to each function, or if customization uis only required per instance of a struct like this:

 struct foo {
    ...
    void (error_handler)(char *);
 };

 void default_error_handler(char *message) { 
    assert(f);
    printf("%s", message);
 }

 void foo_set_error_handler(struct foo *f, void (*eh)(char *)) {
    assert(f);
    f->error_handler = eh;
 }

 struct foo *foo_init() {
    struct foo *f = malloc(sizeof(struct foo));
    foo_set_error_handler(f, default_error_handler);
    return f;
 }


 struct foo *f = foo_init();
 foo_something();

One interesting benefit of a callback is that it can be invoked multiple times, or none at all in the absence of errors in which there is no overhead on the happy path.

There is, however, an inversion of control. The calling code does not know if the callback was invoked. As such, it may make sense to also use an indicator.

查看更多
墨雨无痕
4楼-- · 2019-01-01 17:18

What you could do instead of returning your error, and thus forbidding you from returning data with your function, is using a wrapper for your return type:

typedef struct {
    enum {SUCCESS, ERROR} status;
    union {
        int errCode;
        MyType value;
    } ret;
} MyTypeWrapper;

Then, in the called function:

MyTypeWrapper MYAPIFunction(MYAPIHandle h) {
    MyTypeWrapper wrapper;
    // [...]
    // If there is an error somewhere:
    wrapper.status = ERROR;
    wrapper.ret.errCode = MY_ERROR_CODE;

    // Everything went well:
    wrapper.status = SUCCESS;
    wrapper.ret.value = myProcessedData;
    return wrapper;
} 

Please note that with the following method, the wrapper will have the size of MyType plus one byte (on most compilers), which is quite profitable; and you won't have to push another argument on the stack when you call your function (returnedSize or returnedError in both of the methods you presented).

查看更多
爱死公子算了
5楼-- · 2019-01-01 17:18

EDIT:If you need access only to the last error, and you don't work in multithreaded environment.

You can return only true/false (or some kind of #define if you work in C and don't support bool variables), and have a global Error buffer that will hold the last error:

int getObjectSize(MYAPIHandle h, int* returnedSize);
MYAPI_ERROR LastError;
MYAPI_ERROR* getLastError() {return LastError;};
#define FUNC_SUCCESS 1
#define FUNC_FAIL 0

if(getObjectSize(h, &size) != FUNC_SUCCESS ) {
    MYAPI_ERROR* error = getLastError();
    // error handling
}
查看更多
孤独总比滥情好
6楼-- · 2019-01-01 17:20

I was pondering this issue recently as well, and wrote up some macros for C that simulate try-catch-finally semantics using purely local return values. Hope you find it useful.

查看更多
长期被迫恋爱
7楼-- · 2019-01-01 17:21

First approach is better IMHO:

  • It's easier to write function that way. When you notice an error in the middle of the function you just return an error value. In second approach you need to assign error value to one of the parameters and then return something.... but what would you return - you don't have correct value and you don't return error value.
  • it's more popular so it will be easier to understand, maintain
查看更多
登录 后发表回答