As with a lot of things, error handling in C requires more care than in other (higher-level) languages.
Personally, I do not believe that's necessarily a bad thing as it forces you to actually think about error conditions, the associated cotrol flow and you'll need to come up with a proper design (because if you don't, the result will probably be an unmaintainable mess).
Roughly, you can divide error conditions into fatal and non-fatal ones.
Of the non-fatal ones, there are those that are recoverable, ie you just try again or use a fallback mechanism. Those, you normally handle inline, even in languages that support exceptions.
Then, there are the ones you cannot recover from. Instead, you might want to log the failure, but in genereal just notify the caller, eg via a return code or some errno
type variable. Possibly, you might need to do some cleanup within the function, where a goto
might help to structure the code more cleanly.
Of the fatal ones, there are those that terminate the program, ie you just print some error message and exit()
with non-zero status.
Then, there are the exceptional cases, where you just dump core (eg via abort()
). Assertion failures are a subset of those, but you only should use assert()
if the situation is supposed to be impossible during normal execution so your code still fails cleanly on NDEBUG
builds.
A third class of fatal exceptions is those that do not terminate the whole program, but just a (possibly deeply nested) call chain. You can use longjmp()
here, but you must take care to properly clean up resources like allocated memory or file descriptors, which means you need to keep track of these resources in some pools.
A bit of variadic macro magic makes it possible to come up with nice syntax for at least some of these cases, eg
_Bool do_stuff(int i);
do_stuff(i) || panic("failed to do stuff with %i", i);