What is the correct way to implement error propagation in C?

3.5k views Asked by At

I come from high-level languages with exceptions (C#, in particular), and I usually only catch exceptions that my logic knows how to handle — otherwise, they propagate and program crashes, but at least it provides a clear information about what happened, and where. I want to implement similar behavior in C. I want, in the small body of my main, to get an error code from the functions that I call, to identify what happened, and to print error message to stderr and quit with the same error code as I got.

  1. First question — do people do that in C, and is it a good objective, in terms of architecture, at all?

  2. Seconds question is about error return codes and errno. What should I use in my code by default? It seems that these two conventions are used interchangeably in C standard library, and it's pretty confusing.

  3. Third question is about error codes themselves. In C# and Java, there are standard exception types that cover almost everything you would typically throw, apart from specific comments. Should I use errno error code table for this purpose, or should I create a separate error code table for my application to include all stuff that it specific to it? Or may be every function should maintain it's own specific table of error codes?

  4. And finally, additional information about errors: I found that in C#, at least, it's very useful to be able to throw an exception of predefined type with a specific comment that provides additional information. Is something like this used in C error management? How should I implement it?

  5. Why do library functions use return codes and errno at the same time instead of sticking to one or the other? What's the benefit in using both?

3

There are 3 answers

3
Sourav Ghosh On

It seems, you can make use of errno variable and perror() functions. They help to provide the reason for failure for most of the time.

Most of the library functions, to indicate the failure, will usually return a prefixed negative value (mostly -1) and set the errno variable to a particular value, depending on the type of the error.

is it normally used for this purpose?

Yes, that is the purpose for having errno and perror(), IMHO.

Now, in case of user-defined functions, you can create own error codes for the errors returned by the library functions (with some time of mapping) and you can return your own value from your function.


EDIT:

What's the benefit in using both?

To differentiate the reason for failure, in case of a failure event, in an easy way.

Think it like this, instead of having a fixed reurn value for error and setting errno, if a function returns different values for each type of error, then how many if-else or switch statements you'll need for checking the success of the called function, each time it's called? It's huge.

Rather, use a fixed return value to indicate error and if error, then check the errno for the reason behind the error.

[Hope I'm clear. English is not my native language, sorry]

5
ryyker On

Regarding: I want, in the small body of my main, to get an error code from the functions that I call, to identify what happened, and to print error message to stderr and quit with the same error code as I got

First question — do people do that in C, and is it a good objective, in terms of architecture, at all?

Some people do this, but All people should. It is considered good form

Seconds question is about error return codes and errno. What should I use in my code by default? It seems that these two conventions are used interchangeably in C standard library, and it's pretty confusing.

See first answer. It covers this well. (+1 to @Sourav)

Third question is about error codes themselves. In C# and Java, there are standard exception types that cover almost everything you would typically throw, apart from specific comments. Should I use errno error code table for this purpose, or should I create a separate error code table for my application to include all stuff that it specific to it? Or may be every function should maintain it's own specific table of error codes?

This is completely up to the developer. When creating an application, I will use specific error codes/messages native to the library call that generated the condition. When I create an API (usually .dll) I will often create a single enum containing all possible return conditions specific to the application, and will integrate C native library errors into this enum as well. Zero/Positive values for success conditions, and negative for error conditions. Along with that I create an array of string descriptions corresponding to each return condition. These are callable by the application via a function, eg. int GetErrorDescription(int error, char *str);. However, this can be approached several different ways, this is just my approach.

Example:

/*------------------------------------------
//List of published error codes
/*-----------------------------------------*/
enum    {
    SUCCESS                        =  0,
    COPYFILE_ERR_1                 = -1,   //"CopyFile() error. File not found or directory in path not found.",
    COPYFILE_ERR_3                 = -2,   //"CopyFile() error. General I/O error occurred.",
    COPYFILE_ERR_4                 = -3,   //"CopyFile() error. Insufficient memory to complete operation.",
    COPYFILE_ERR_5                 = -4,   //"CopyFile() error. Invalid path or target and source are same.",
    COPYFILE_ERR_6                 = -5,   //"CopyFile() error. Access denied.",
    ...
    FOPEN_ERR_EIO                  = -11,  //"fopen() error. I/O error.",
    FOPEN_ERR_EBADF                = -12,  //"fopen() error. Bad file handle.",
    FOPEN_ERR_ENOMEM               = -13,  //"fopen() error. Insufficient memory.",
    ...
    GETFILESIZE_ERR_3              = -24,  //"GetFileSize() error - Insufficient memory to complete operation.",
    GETFILESIZE_ERR_4              = -25,  //"GetFileSize() error - Invalid path or target and source are same.",
    GETFILESIZE_ERR_5              = -26,  //"GetFileSize() error - Access denied.",
    ...
    PATH_MUST_BE_C_DRIVE           = -38,  //"SaveFile path variable must contain \"c:\\\".",
    HEADER_FIELD_EMPTY             = -39,  //"one or more of the header fields are incorrect or empty.",
    HEADER_FIELD_ILLEGAL_COMMA     = -40,  //"one or more of the header fields contains a comma.",
    ...
    MAX_ERR                        =  46   // defines the size of the static char ErrMsg
};

And then a function to get the description:

int API GetErrorMessage (int err, char * retStr)
{
    //verify arguments are not NULL
    if(retStr == NULL)
    {
        return UNINIT_POINTER_ARGUMENT;
    }
    switch(err) {
        case  COPYFILE_ERR_1               : strcpy(retStr, "CopyFile() error. File not found or directory in path not found."     ); break;
        case  COPYFILE_ERR_3               : strcpy(retStr, "CopyFile() error. General I/O error occurred."                        ); break;
        case  COPYFILE_ERR_4               : strcpy(retStr, "CopyFile() error. Insufficient memory to complete operation."         ); break;
        case  COPYFILE_ERR_5               : strcpy(retStr, "CopyFile() error. Invalid path or target and source are same."        ); break;
        case  COPYFILE_ERR_6               : strcpy(retStr, "CopyFile() error. Access denied."                                     ); break;
        ...
        case  FOPEN_ERR_EIO                : strcpy(retStr, "fopen() error. I/O error."                                            ); break;
        case  FOPEN_ERR_EBADF              : strcpy(retStr, "fopen() error. Bad file handle."                                      ); break;
        case  FOPEN_ERR_ENOMEM             : strcpy(retStr, "fopen() error. Insufficient memory."                                  ); break;
        ...
        case  GETFILESIZE_ERR_3            : strcpy(retStr, "GetFileSize() error - Insufficient memory to complete operation."     ); break;
        case  GETFILESIZE_ERR_4            : strcpy(retStr, "GetFileSize() error - Invalid path or target and source are same."    ); break;
        case  GETFILESIZE_ERR_5            : strcpy(retStr, "GetFileSize() error - Access denied."                                 ); break;
        ...
        case  HEADER_FIELD_EMPTY           : strcpy(retStr, " one or more of the header fields are incorrect or empty."        ); break;
        case  HEADER_FIELD_ILLEGAL_COMMA   : strcpy(retStr, " one or more of the header fields contains a comma."              ); break;
        case  EVENT_FIELD_EMPTY            : strcpy(retStr, " one or more of the event fields is incorrect or empty."          ); break;
        case  EVENT_FIELD_ILLEGAL_COMMA    : strcpy(retStr, " one or more of the event fields contains a comma."               ); break;
        ...
        default:   strcpy(retStr, ""); break;
    }
    return 0;
}

And finally, additional information about errors: I found that in C#, at least, it's very useful to be able to throw an exception of predefined type with a specific comment that provides additional information. Is something like this used in C error management? How should I implement it?

comment addressing previous question also addresses this.

Why do library functions use return codes and errno at the same time instead of sticking to one or the other? What's the benefit in using both?

again, covered by Sourav.

4
dbush On

Generally speaking, exception-type behavior isn't done in C. The convention is that the return value of each function is expected to be checked and handled by the immediate caller, along with any ancillary variable (such as errno) that the function may set.

That being said, you could do something similar to exceptions by using setjmp() and longjmp(). You use setjmp() to set a jump point, passing in a jmp_buf structure to save the jump point. Later on in the code, no matter how far down the call stack, you can pass that jmpbuf to longjmp() which will result in control going back up to where setjmp() was called. You also pass a value to longjmp() which will be the return value of setjmp(). You can then use this value as a lookup into an error table to print a more detailed message.

There are several other questions related to longjmp() that you can read up on.

EDIT:

An example:

jmp_buf jbuf;

void some_function()
{
   ...
   if (some_error()) {
     longjmp(jbuf, 2);
     /* control never gets here */
   }
   ...
}

void run_program()
{
  ...
  some_function();
  ...
}

int main()
{
  int rval;
  if ((rval=setjmp(jbuf)) == 0) {
    run_program();
  } else {
    /* rval will be 2 if the error in some_function() is triggered */
    printf("error %d: %s\n", rval, get_err_string(rval));
  }
}