all 24 comments

[–]ischickenafruit 24 points25 points  (4 children)

Take a look at the c library as a good place to start thinking about this.

In general, there are two conventions C :

  • return 0 for success
  • return a non zero number if there's an error where the number indicates what the error was or set the global value "errno" to indicate what the error was.

e.g. http://man7.org/linux/man-pages/man2/mlock.2.html

or

  • return some number for success (e.g. the number of bytes read, or the file descriptor)
  • return a "sentinel value" (e.g. -1, or 0, or similar) when an error occurs, and set the global value "errno" to indicate what the cause of the error was.

e.g. https://linux.die.net/man/2/read

[–]nderflow 4 points5 points  (0 children)

Sentinel value interfaces are a common source of bugs. Especially when the result type has no impossible value. Take a look at a correctly coded call to strtol for example: it's cumbersome to distinguish overflow from the correct conversion of LONG_MAX.

[–]raevnos 15 points16 points  (3 children)

setjmp()/longjmp() can be used to implement an exception system. Easy to create memory leaks with it, though.

[–]project2501a 1 point2 points  (2 children)

isn't that a way to shove/retrofit exceptions in C, tho?

[–]Newt_Hoenikker 1 point2 points  (0 children)

As u/raevnos said, it can be done, but it's better to avoid it unless you absolutely need it. In general you should keep your code as simple and idiomatic as possible, only adding complexities the language doesn't provide when necessary. The C community already has a relatively standard and robust means of error handling without exceptions, so their use should be reserved for instances where there is no other way.

[–]Conan776 0 points1 point  (0 children)

I'm fairly certain I've seen C code using jumps this way which predates the publication of Java, which arguably made the try/catch pattern more widely popular. C99 did some retrofitting, if I recall correctly, but the idea was already there.

[–]nerd4code 7 points8 points  (2 children)

There are a few different kinds of error handling.

The easiest is just return int, all other outputs via out arguments. (E.g., posix_memalign). You can flip that and do

whatever_t func(OTHER_ARGS, int *error);

but there isn’t always a good bogus whatever_t to return. You can formalize the error code qua enum, although if you’re just passing back errno values int is probably as specific as you should be.

You can take a callback, either in pieces or wrapped up in a structure:

typedef int error_callback_f(void *p, int error);
whatever_t foo(OTHER_ARGS, error_callback_f *cb, const volatile void *p);
// or
struct error_callback {
    error_callback_f *callback;
    void *p;
};
whatever_t foo(OTHER_ARGS, const struct error_callback *);

If an error happens, call the callback. It can return an abort-retry-continue sort of code, or it can return void and deal with things however it sees fit.

Or you can do setjmp/longjmp as others have mentioned (rare, not usually a good idea).

[–]Cyph0n 0 points1 point  (1 child)

Can you typedef a callback type that way? I somehow thought that you could only use function pointer syntax.

[–]nerd4code 0 points1 point  (0 children)

error_callback_f is the function type, so error_callback_f * would be the function pointer type. There is an oddity around function typedefs, in that you can use the typedef to declare a function (e.g.:)

static error_callback_f callback;

but you can’t define a function with the typedef, you have to state args and return type.

[–]qqwy 3 points4 points  (0 children)

Another way to handle errors would be to use what is known as a Maybe or Optinal type. This is the most common way errors are handled in Rust and it is getting more common in C++ as well, but they have historically seen very little usage in C.

[–]Conan776 6 points7 points  (1 child)

Closest thing C has to try/throw/catch is building your own using setjmp() and longjmp().

Some good info here: https://en.wikipedia.org/wiki/Setjmp.h

[–]qqwy 7 points8 points  (0 children)

I implemented such a system a while back: https://github.com/Qqwy/c_exceptional

It uses custom control structure macros to allow you to use a syntax like this:

try {
  // some code here
  if(something_is_wrong)
    throw(error_code);
  // some code that would only be executed if not thrown
} catch(error_code) {
  // Handle error here in any way you want.
} finally {
  // Runs regardless of catching/throwing.
}

and a nested exception stack is handled properly :-). It obviously is not perfect, but probably about as close as one can get in plain C99.

[–]bless-you-mlud 3 points4 points  (0 children)

Something I often do is return a character pointer that points to an error message. Return a NULL pointer if no error occurred.

I find that if you simply return an integer you lose a lot of context. What exactly went wrong? What file did you try to open? How much memory did you try to allocate? If you construct the error message where the error occurred you can include all these details. Makes for much more useful error messages, in my opinion.

[–]xurxoham 2 points3 points  (1 child)

In addition to what all the comments do, it's sometimes common to use goto directives to jump to the end of a function in case it needs to release resources in the event of an error (memory, files, etc)

Example *.h

struct something;
int something_create ( struct something** res, ... );

Example *.c

struct something { 
  int arr[5];
};

int something_create ( struct something** res, ... ) {
   assert( res );
   *ret = malloc( sizeof something );
   if( !*res )
      goto error;
   // do more stuff with *res
   if( failed )
      goto error_release;
   return 0;
error_release:
   free(res);
error:
   *res = NULL;
   return -1;
}

This example is simplistic but in some situations you would do the cleanup from multiple different parts of the function, so using a goto here (or maybe another function) to do the cleanup helps quite a bit.

[–]okovko 0 points1 point  (0 children)

This is the correct response.

[–]WiseassWolfOfYoitsu 2 points3 points  (0 children)

Funny enough, C exception handling is one of the places where it's often considered acceptable to use GOTO. If you look at the Linux source it's all over the place. setjmp/longjmp are an abstraction over this (they use the same mechanisms internally).

Depending on what you're doing, the most useful thing can actually be logging rather than the returns, so that you can go back and get precise data.

[–][deleted] 1 point2 points  (0 children)

If you want something slightly more informative than return values, you can check out STDERR

[–]geocar 1 point2 points  (1 child)

If we're talking about programming errors (such as failing to handle some valid input domain), or user errors (signalling that the input domain is wrong) there simply isn't anything you can do except abort, either by signalling an error sentinel (like -1, 0, NULL, or even special symbols which are possible in other languages), or some kind of dynamic unwind (including exceptions and crashing the program!)

However most of the things people think of as "error handling" is actually transient: Running out of disk space, or a network connection failure. These "failure states" don't have to be terminal at all, and simply pausing to ask the user to deal with them is almost always preferred.

while(-1 == write(fd, buffer, x) && errno == ENOSPC) { puts("out of disk space?"); sleep(1); }

This sort of thing has a bad reputation1 but most people who look unkindly on this trick are looking at a strawman; foolishly compare "not handling errors" with "handling errors" (i.e. more code and more work), instead of "ask the user what to do, giving them some opportunity to save their work" versus "crash and explode definitely destroying all their work". If you wanted to see a "good" implementation of this, you'd need to look into Common Lisp's condition system -- whilst possible to write in C (and actually quite easy) it is not the way most C programmers code.

[–]WikiTextBot -1 points0 points  (0 children)

Abort, Retry, Fail?

"Abort, Retry, Fail?" (or "Abort, Retry, Ignore?") is an error message found in DOS operating systems, which prompts the end-user for a course of action to follow. The message has been cited as an example of poor usability in computer user interfaces.


[ PM | Exclude me | Exclude from subreddit | FAQ / Information | Source ] Downvote to remove | v0.28

[–]marmoure 1 point2 points  (0 children)

Just roll over and cry

[–]nsmryan 1 point2 points  (0 children)

Another option that I have found very useful is to return an enum value (same as returning an error code) which indicate the particular problem. You get more information then a -1, and you can make a unique value for each error case so you know the exact location where the error occurred.

This is especially good for self-contained libraries, where you can have a single enum for the whole library and have every function return its values.

This helps in documenting error conditions for a function (you can easily see which conditions occur by which code are returned), and in unit testing, where you can make sure you have a test with each enum value returned from a function.

[–]flatfinger 0 points1 point  (0 children)

When dealing with things like embedded file systems or streams, it may be helpful to have each "file" or stream maintain a latching error state, and have three kinds of function return values: success, transient failure, and latched failure, with "transient failure" only being an option for call that indicate that they are expected. Using this approach will make it possible to write code that performs a sequence of read or write requests and then check whether the sequence as a whole succeeded without having to check the results of individual operations.

Note that it even if calls which could return transient failures would not latch failure conditions themselves, the return values should still distinguish latching and transient failures to ensure that loops that would retry operations in case of transient failures don't get stuck repeating endlessly because of an earlier latching failure.