Is there a better way to do C style error handling?
Asked Answered
A

10

12

I'm trying to learn C by writing a simple parser / compiler. So far its been a very enlightening experience, however coming from a strong background in C# I'm having some problems adjusting - in particular to the lack of exceptions.

Now I've read Cleaner, more elegant, and harder to recognize and I agree with every word in that article; In my C# code I avoid throwing exceptions whenever possible, however now that I'm faced with a world where I can't throw exceptions my error handling is completely swamping the otherwise clean and easy-to-read logic of my code.

At the moment I'm writing code which needs to fail fast if there is a problem, and it also potentially deeply nested - I've settled on a error handling pattern whereby "Get" functions return NULL on an error, and other functions return -1 on failure. In both cases the function that fails calls NS_SetError() and so all the calling function needs to do is to clean up and immediately return on a failure.

My issue is that the number of if (Action() < 0) return -1; statements that I have is doing my head in - it's very repetitive and completely obscures the underlying logic. I've ended up creating myself a simple macro to try and improve the situation, for example:

#define NOT_ERROR(X)    if ((X) < 0) return -1

int NS_Expression(void)
{
    NOT_ERROR(NS_Term());
    NOT_ERROR(Emit("MOVE D0, D1\n"));

    if (strcmp(current->str, "+") == 0)
    {
        NOT_ERROR(NS_Add());
    }
    else if (strcmp(current->str, "-") == 0)
    {
        NOT_ERROR(NS_Subtract());
    }
    else
    {
        NS_SetError("Expected: operator");
        return -1;
    }
    return 0;
}

Each of the functions NS_Term, NS_Add and NS_Subtract do a NS_SetError() and return -1 in the case of an error - its better, but it still feels like I'm abusing macros and doesn't allow for any cleanup (some functions, in particular Get functions that return a pointer, are more complex and require clean-up code to be run).

Overall it just feels like I'm missing something - despite the fact that error handling in this way is supposedly easier to recognize, In many of my functions I'm really struggling to identify whether or not errors are being handled correctly:

  • Some functions return NULL on an error
  • Some functions return < 0 on an error
  • Some functions never produce an error
  • My functions do a NS_SetError(), but many other functions don't.

Is there a better way that I can structure my functions, or does everyone else also have this problem?

Also is having Get functions (that return a pointer to an object) return NULL on an error a good idea, or is it just confusing my error handling?

Autopilot answered 15/4, 2011 at 13:15 Comment(4)
I used to think returning NULL on failure was a good idea, until recently when I worked on some inherited code where functions could return NULL for all kinds of reasons. It's not fun debugging null pointers :)Riflery
Abusing macros? You've never seen real macro abuse, sonny.Garber
@Garber The macros I've seen have led to a phobia of macros, hence my deep desire to avoid them! :-)Autopilot
@Riflery The thing that led me in the direction of returning NULL was SDL. It seemed like a neat pattern as it means you don't need to separately test the out pointer for a NULL.Autopilot
T
6

One technique for cleanup is to use an while loop that will never actually iterate. It gives you goto without using goto.

#define NOT_ERROR(x) if ((x) < 0) break;
#define NOT_NULL(x) if ((x) == NULL) break;

// Initialise things that may need to be cleaned up here.
char* somePtr = NULL;

do
{
    NOT_NULL(somePtr = malloc(1024));
    NOT_ERROR(something(somePtr));
    NOT_ERROR(somethingElse(somePtr));
    // etc

    // if you get here everything's ok.
    return somePtr;
}
while (0);

// Something went wrong so clean-up.
free(somePtr);
return NULL;

You lose a level of indentation though.

Edit: I'd like to add that I've nothing against goto, it's just that for the use-case of the questioner he doesn't really need it. There are cases where using goto beats the pants off any other method, but this isn't one of them.

Ternan answered 15/4, 2011 at 13:23 Comment(11)
can you explain a bit more deeply?Dowling
Using the 'break' functionality of while loops lets you go straight to where you need to clean-up. Can whomever down voted this please explain why?Ternan
I'm also interested to know why this got down-voted - to me it looks similar to the goto examples, just without the goto.Autopilot
@james: oh, clever! +1 from me :)Dowling
@Kragen said in another way: "similar to 10, but expressed as 1+2+3+4" ("just without the 10"). +1, I like this idea too, however I dislike that you consider it better "because it's without goto".Vicennial
@Johannes Schaub: Not a fan of try... catch?Ternan
What if you have multiple possible error conditions, some that share cleanup and some that don't? This will work in simple cases, but even then seems like a rather obtuse work-around for a fairly simple (and arguably clearer) goto.Onder
There's probably not enough space to give an example in the comments but I'd like to see one.Ternan
This solution fails if you need to do error checking inside of a loop or another while. The macros' break will break out of the loop/while but not the outer while (0).Aneroid
the code snippet is not a best practice at all, there is an over engineering with the loop. All logic could be done without loop, it makes no sense, it is a place to do an error in future. I would strictly recommend to avoid such macro expressions. if you really want to reduce some constant NULL conditions checks redefine the memory allocation functions with your hooks inside to check the NULL and make some save action and exit, because program normally should not have any NULL pointer recoverable errors in termes of memory allocation. If there are some then the should be handled explicitly.Spam
@ykhrustalev: Ok, you've not explained your point very well but about the macros... I personally wouldn't use them either but I put them in becuase the questioner had them. I also don't see how it can introduce errors in the future; if everything succeeds the execution will follow one path and if it fails it'll follow another.Ternan
I
13

It's a bigger problem when you have to repeat the same finalizing code before each return from an error. In such cases it is widely accepted to use goto:

int func ()
{
  if (a() < 0) {
    goto failure_a;
  }

  if (b() < 0) {
    goto failure_b;
  }

  if (c() < 0) {
    goto failure_c;
  }

  return SUCCESS;

  failure_c:
  undo_b();

  failure_b:
  undo_a();

  failure_a:
  return FAILURE;
}

You can even create your own macros around this to save you some typing, something like this (I haven't tested this though):

#define CALL(funcname, ...) \
  if (funcname(__VA_ARGS__) < 0) { \ 
    goto failure_ ## funcname; \
  }

Overall, it is a much cleaner and less redundant approach than the trivial handling:

int func ()
{
  if (a() < 0) {
    return FAILURE;
  }

  if (b() < 0) {
    undo_a();
    return FAILURE;
  }

  if (c() < 0) {
    undo_b();
    undo_a();
    return FAILURE;
  }

  return SUCCESS;
}

As an additional hint, I often use chaining to reduce the number of if's in my code:

if (a() < 0 || b() < 0 || c() < 0) {
  return FAILURE;
}

Since || is a short-circuit operator, the above would substitute three separate if's. Consider using chaining in a return statement as well:

return (a() < 0 || b() < 0 || c() < 0) ? FAILURE : SUCCESS;
Isostasy answered 15/4, 2011 at 13:31 Comment(0)
T
6

One technique for cleanup is to use an while loop that will never actually iterate. It gives you goto without using goto.

#define NOT_ERROR(x) if ((x) < 0) break;
#define NOT_NULL(x) if ((x) == NULL) break;

// Initialise things that may need to be cleaned up here.
char* somePtr = NULL;

do
{
    NOT_NULL(somePtr = malloc(1024));
    NOT_ERROR(something(somePtr));
    NOT_ERROR(somethingElse(somePtr));
    // etc

    // if you get here everything's ok.
    return somePtr;
}
while (0);

// Something went wrong so clean-up.
free(somePtr);
return NULL;

You lose a level of indentation though.

Edit: I'd like to add that I've nothing against goto, it's just that for the use-case of the questioner he doesn't really need it. There are cases where using goto beats the pants off any other method, but this isn't one of them.

Ternan answered 15/4, 2011 at 13:23 Comment(11)
can you explain a bit more deeply?Dowling
Using the 'break' functionality of while loops lets you go straight to where you need to clean-up. Can whomever down voted this please explain why?Ternan
I'm also interested to know why this got down-voted - to me it looks similar to the goto examples, just without the goto.Autopilot
@james: oh, clever! +1 from me :)Dowling
@Kragen said in another way: "similar to 10, but expressed as 1+2+3+4" ("just without the 10"). +1, I like this idea too, however I dislike that you consider it better "because it's without goto".Vicennial
@Johannes Schaub: Not a fan of try... catch?Ternan
What if you have multiple possible error conditions, some that share cleanup and some that don't? This will work in simple cases, but even then seems like a rather obtuse work-around for a fairly simple (and arguably clearer) goto.Onder
There's probably not enough space to give an example in the comments but I'd like to see one.Ternan
This solution fails if you need to do error checking inside of a loop or another while. The macros' break will break out of the loop/while but not the outer while (0).Aneroid
the code snippet is not a best practice at all, there is an over engineering with the loop. All logic could be done without loop, it makes no sense, it is a place to do an error in future. I would strictly recommend to avoid such macro expressions. if you really want to reduce some constant NULL conditions checks redefine the memory allocation functions with your hooks inside to check the NULL and make some save action and exit, because program normally should not have any NULL pointer recoverable errors in termes of memory allocation. If there are some then the should be handled explicitly.Spam
@ykhrustalev: Ok, you've not explained your point very well but about the macros... I personally wouldn't use them either but I put them in becuase the questioner had them. I also don't see how it can introduce errors in the future; if everything succeeds the execution will follow one path and if it fails it'll follow another.Ternan
O
5

You're probably not going to like to hear this, but the C way to do exceptions is via the goto statement. This is one of the reasons it is in the language.

The other reason is that goto is the natural expression of the implementation of a state machine. What common programming task is best represented by a state machine? A lexical analyzer. Look at the output from lex sometime. Gotos.

So it sounds to me like now is the time for you to get chummy with that parriah of language syntax elements, the goto.

Onder answered 15/4, 2011 at 13:24 Comment(6)
There are ways of doing it without explicitly using goto.Ternan
@Ternan - Yes, there are. However, as things get complicated those ways get more and more awkward. At some point you are just much better off admitting that your control flow is unstructured rather than trying to obscure it further so you can pretend it isn't.Onder
We'll have to agree to disagree then! It obscures control flow no more than using a try {} catch block.Ternan
@Ternan - Check the tags. This is a C question. try/catch is not available.Onder
I thought you'd say that, but the program flow is exactly the same and try/catch works.Ternan
@Ternan - Does it? Last I checked exceptions weren't in the C standard. I admit I haven't kept up with it though.Onder
S
4

What about this?

int NS_Expression(void)
{
    int ok = 1;
    ok = ok && NS_Term();
    ok = ok && Emit("MOVE D0, D1\n");
    ok = ok && NS_AddSub();
    return ok
}
Semitic answered 15/4, 2011 at 14:11 Comment(5)
This would mean that if NS_Term failed, we would still go ahead and call Emit and NS_AddSub anyway, potentially leading to Bad Things happening.Autopilot
You sure? None of my programs would work... In that code I assume that those functions return 1 if everything is ok... The odd thing is the return value of NS_Expression, should return ok instead, I'll edit it.Semitic
Oh, wait... Yeah I see it now - sorry, its late here!Autopilot
clever, but what if you have other statements in between? Couldn't you just write this as return NS_Term() && Emit("MOVE D0, D1\n") && NS_AddSub();?Isostasy
If you need to add something that doesn't fit that scheme you can always add a if (ok) {...}. You could write it as you suggest, but in the long term you'll need to add some code that doesn't fit that scheme, so I prefer that. Any smart compiler should generate the same code for both.Semitic
A
4

Besides goto, standard C has another construct to handle exceptional flow control setjmp/longjmp. It has the advantage that you can break out of multiply nested control statements more easily than with break as was proposed by someone, and in addition to what goto provides has a status indication that can encode the reason for what went wrong.

Another issue is just the syntax of your construct. It is not a good idea to use a control statement that can inadvertibly be added to. In your case

if (bla) NOT_ERROR(X);
else printf("wow!\n");

would go fundamentally wrong. I'd use something like

#define NOT_ERROR(X)          \
  if ((X) >= 0) { (void)0; }  \
  else return -1

instead.

Archibaldo answered 15/4, 2011 at 14:11 Comment(0)
G
3

THis must be thought on at least two levels: how your functions interact, and what you do when it breaks.

Most large C frameworks I see always return a status and "return" values by reference (this is the case of the WinAPI and of many C Mac OS APIs). You want to return a bool?

StatusCode FooBar(int a, int b, int c, bool* output);

You want to return a pointer?

StatusCode FooBar(int a, int b, int c, char** output);

Well, you get the idea.

On the calling function's side, the pattern I see the most often is to use a goto statement that points to a cleanup label:

    if (statusCode < 0) goto error;

    /* snip */
    return everythingWentWell;

error:
    cleanupResources();
    return somethingWentWrong;
Garber answered 15/4, 2011 at 13:33 Comment(0)
P
3

The short answer is: let your functions return an error code that cannot possibly be a valid value - and always check the return value. For functions returning pointers, this is NULL. For functions returning a non-negative int, it's a negative value, commonly -1, and so on...

If every possible return value is also a valid value, use call-by-reference:

int my_atoi(const char *str, int *val)
{
        // convert str to int
        // store the result in *val
        // return 0 on success, -1 (or any other value except 0) otherwise
}


Checking the return value of every function might seem tedious, but that's the way errors are handled in C. Consider the function nc_dial(). All it does is checking its arguments for validity and making a network connection by calling getaddrinfo(), socket(), setsockopt(), bind()/listen() or connect(), finally freeing unused resources and updating metadata. This could be done in approximately 15 lines. However, the function has nearly 100 lines due to error checking. But that's the way it is in C. Once you get used to it, you can easily mask the error checking in your head.

Furthermore, there's nothing wrong with multiple if (Action() == 0) return -1;. To the contrary: it is usually a sign of a cautious programmer. It's good to be cautious.

And as a final comment: don't use macros for anything but defining values if you can't justify their use while someone is pointing with a gun at your head. More specifically, never use control flow statements in macros: it confuses the shit out of the poor guy who has to maintain your code 5 years after you left the company. There's nothing wrong with if (foo) return -1;. It's simple, clean and obvious to the point that you can't do any better.

Once you drop your tendency to hide control flow in macros, there's really no reason to feel like you're missing something.

Polestar answered 16/4, 2011 at 1:42 Comment(0)
L
2

A goto statement is the easiest and potentially cleanest way to implement exception style processing. Using a macro makes it easier to read if you include the comparison logic inside the macro args. If you organize the routines to perform normal (i.e. non-error) work and only use the goto on exceptions, it is fairly clean for reading. For example:

/* Exception macro */
#define TRY_EXIT(Cmd)   { if (!(Cmd)) {goto EXIT;} }

/* My memory allocator */
char * MyAlloc(int bytes)
{
    char * pMem = NULL;

    /* Must have a size */
    TRY_EXIT( bytes > 0 );

    /* Allocation must succeed */
    pMem = (char *)malloc(bytes);
    TRY_EXIT( pMem != NULL );

    /* Initialize memory */
    TRY_EXIT( initializeMem(pMem, bytes) != -1 );

    /* Success */
    return (pMem);

EXIT:

    /* Exception: Cleanup and fail */
    if (pMem != NULL)
        free(pMem);

    return (NULL);
}
Lightheaded answered 15/4, 2011 at 14:11 Comment(0)
A
1

It never occurred to me to use goto or do { } while(0) for error handling in this way - its pretty neat, however after thinking about it I realised that in many cases I can do the same thing by splitting the function out into two:

int Foo(void)
{
    // Initialise things that may need to be cleaned up here.
    char* somePtr = malloc(1024);
    if (somePtr = NULL)
    {
        return NULL;
    }

    if (FooInner(somePtr) < 0)
    {
        // Something went wrong so clean-up.
        free(somePtr);
        return NULL;
    }

    return somePtr;
}

int FooInner(char* somePtr)
{
    if (something(somePtr) < 0) return -1;
    if (somethingElse(somePtr) < 0) return -1;
    // etc

    // if you get here everything's ok.
    return 0;
}

This does now mean that you get an extra function, but my preference is for many short functions anyway.

After Philips advice I've also decided to avoid using control flow macros as well - its clear enough what is going on as long as you put them on one line.

At the very least Its reassuring to know that I'm not just missing something - everyone else has this problem too! :-)

Autopilot answered 16/4, 2011 at 3:55 Comment(0)
M
0

Use setjmp.

http://en.wikipedia.org/wiki/Setjmp.h

http://aszt.inf.elte.hu/~gsd/halado_cpp/ch02s03.html

http://www.di.unipi.it/~nids/docs/longjump_try_trow_catch.html

#include <setjmp.h>
#include <stdio.h>

jmp_buf x;

void f()
{
    longjmp(x,5); // throw 5;
}

int main()
{
    // output of this program is 5.

    int i = 0;

    if ( (i = setjmp(x)) == 0 )// try{
    {
        f();
    } // } --> end of try{
    else // catch(i){
    {
        switch( i )
        {
        case  1:
        case  2:
        default: fprintf( stdout, "error code = %d\n", i); break;
        }
    } // } --> end of catch(i){
    return 0;
}

#include <stdio.h>
#include <setjmp.h>

#define TRY do{ jmp_buf ex_buf__; if( !setjmp(ex_buf__) ){
#define CATCH } else {
#define ETRY } }while(0)
#define THROW longjmp(ex_buf__, 1)

int
main(int argc, char** argv)
{
   TRY
   {
      printf("In Try Statement\n");
      THROW;
      printf("I do not appear\n");
   }
   CATCH
   {
      printf("Got Exception!\n");
   }
   ETRY;

   return 0;
}
Medievalist answered 19/4, 2011 at 19:51 Comment(1)
No, lease don't use setjmp/longjmp for error handling. While this code looks simple and good, it fails in more complicated cases where you have to cleanup resources (like open files).Claytor

© 2022 - 2024 — McMap. All rights reserved.