63

As a good programmer one should write robust codes that will handle every single outcome of his program. However, almost all functions from the C library will return 0 or -1 or NULL when there's an error.

It's sometimes obvious that error checking is needed, for example when you try to open a file. But I often ignore error checking in functions such as printf or even malloc because I don't feel necessary.

if(fprintf(stderr, "%s", errMsg) < 0){
    perror("An error occurred while displaying the previous error.");
    exit(1);
}

Is it a good practice to just ignore certain errors, or is there a better way to handle all the errors?

9 Answers9

33

In general, code should deal with exceptional conditions wherever it is appropriate. Yes, this is a vague statement.

In higher level languages with software exception handling this is often stated as "catch the exception in the method where you can actually do something about it." If a file error occurred, maybe you let it bubble up the stack to the UI code that can actually tell the user "your file failed to save to disk." The exception mechanism effectively swallows up "every little error" and implicitly handles it at the appropriate place.

In C, you do not have that luxury. There are a few ways to handle errors, some of which are language/library features, some of which are coding practices.

Is it a good practice to just ignore certain errors, or is there a better way to handle all the errors?

Ignore certain errors? Maybe. For example, it is reasonable to assume that writing to standard output will not fail. If it does fail, how would you tell the user, anyway? Yes, it is a good idea to ignore certain errors, or code defensively to prevent them. For example, check for zero before dividing.

There are ways to handle all, or at least most, errors:

  1. You can use jumps, similar to gotos, for error handling. While a contentious issue among software professionals, there are valid uses for them especially in embedded and performance-critical code (e.g. Linux kernel).
  1. Cascading ifs:

    if (!<something>) {
      printf("oh no 1!");
      return;
    }
    if (!<something else>) {
      printf("oh no 2!");
      return;
    }
    
  2. Test the first condition, e.g. opening or creating a file, then assume subsequent operations succeed.

Robust code is good, and one should check for and handle errors. Which method is best for your code depends on what the code does, how critical a failure is, etc. and only you can truly answer that. However, these methods are battle-tested and used in various open source projects where you can take a look to see how real code checks for errors.

17

However, almost all functions from the C library will return 0 or -1 or NULL when there's an error.

Yes, but you know which function you called, don't you?

You actually have a lot of information that you could put in an error message. You know which function was called, the name of the function that called it, what parameters were passed, and the return value of the function. That's plenty of information for a very informative error message.

You don't have to do this for every function call. But the first time you see the error message "An error occurred while displaying the previous error," when what you really needed was useful information, will be the last time you ever see that error message there, because you're immediately going to change the error message to something informative that will help you troubleshoot the problem.

Robert Harvey
  • 200,592
11

TLDR; you should almost never ignore errors.

The C language lacks a good error handling feature leaving for each library developer to implement its own solutions. More modern languages have exceptions built in which makes this particular problem a lot easier to handle.

But when you're are stuck with C you have no such perks. Unfortunately you'll simply have to pay the price every time you're calling a function which there is a remote possibility of failure. Or else you will suffer much worse consequences such as overwriting data in memory unintentionally. So as a general rule you have to check for errors always.

If you don't check for the return of fprintf you're very likely leaving a bug behind that will in the best case not do what the user expects and worse case explode the entire thing during flight. There's no excuse to undermine yourself that way.

However as a C developer it's also your job to make the code easy to maintain. So sometimes you can safely ignore errors for the sake of clarity if (and only if) they do not pose any threat to the overall behavior of the application..

It's the same problem as doing this:

try
{
    run();
} catch (Exception) {
    // comments expected here!!!
}

If you see that with no good comments inside the empty catch block this is certainly an issue. There's no reason to think a call to malloc() will execute successfully 100% of the time.

Alex
  • 3,248
9

The question is not actually language specific, but rather user specific. Think about the question from a user perspective. The user does something, like typing the name of the program on a command line and hitting enter. What does the user expect? How can they tell if something went wrong? Can they afford to intercede if an error occurs?

In many types of code, such checks are overkill. However, in high reliability safety critical code, such as those for nuclear reactors, pathological error checking and planned recovery paths are part of the day-to-day nature of the job. It's deemed worth the cost to take the time to ask "What happens if X fails? How do I get back to a safe state?" In less reliable code, such as those for video games, you can get away with far less error checking.

Another similar approach is how much can you actually improve on the state by catching the error? I cannot count the number of C++ programs that proudly catch exceptions, only to just rethrow them because they didn't actually know what to do with them... but they knew they were supposed to do exception handling. Such programs gained nothing from the extra effort. Only add error checking code that you think may actually handle the situation better than simply not checking the error code. That being said, C++ has specific rules for handling exceptions that occur during exception handling in order to catch them in a more meaningful way (and by that, I mean calling terminate() to make sure the funeral pyre you have built for yourself lights up in its proper glory)

How pathological can you get? I worked on a program that defined a "SafetyBool" whose true and false values where carefully chosen to have an even distribution of 1's and 0's, and they were chosen so that any one of a number of hardware failures (single bit flips, data bus traces getting broken, etc.) did not cause a boolean to be misinterpreted. Needless to say, I would not claim this to be a general purpose programming practice to be used in any old program.

Cort Ammon
  • 11,917
  • 3
  • 26
  • 35
6
  • Different safety requirements demand different levels of correctness. In aviation or automobile control software all return values will be checked, cf. MISRA.FUNC.UNUSEDRET. In a quick proof of concept which never leaves your machine, probably not.
  • Coding costs time. Too many irrelevant checks in non-saftey-critical software is effort better spent elsewhere. But where is the sweet spot between quality and cost? That depends on the debugging tools and the software complexity.
  • Error handling can obscure control flow and introduce new errors. I quite like Richard "network" Stevens' wrapper functions which at least report errors.
  • Error handling can, rarely, be a performance issue. But most C library calls will take so long that the cost of checking a return value is immeasurably small.
4

Always ask yourself the following question:

If I test and there is an error, can I handle it in a meaningful way?

If the answer is "No", then why would you test for it?

E.g. lets take Hello World:

#include <stdio.h>
int main() {
   printf("Hello, World!");
   return 0;
}

We all know, that printf() may fail. What are you going to do when it fails? Print an error? Do you think you will be able to print an error in cases you were not even able to print "Hello World!"? Okay, you might be able to print an error to stderr as just because you cannot print to stdout doesn't mean you cannot print to stderr but how meaningful would that be? What would the error say? "I couldn't print Hello World! to standard out"? How is that helpful? What do you expect the user of your program to do with that information? Which reaction do you expect from the user? Shall the user fix standard out and call your program again? Maybe he can, more likely he doesn't even know what standard out is when he calls programs that only serve the purpose to print "Hello World!".

Assume you would not catch that error, what will happen? Nothing. Well, the program will not print "Hello World!" but that will be obvious to the user, right? The user will see that no Hello World appeared on the screen, right? You don't have to tell the user that.

Of course, if the only task of your program is to print "Hello World!" and it cannot do that, you may argue that your program has failed. And there is a meaningful, standard way to communicate to the caller, that a program has failed:

#include <stdio.h>
int main() {
   int printRes = printf("Hello, World!");
   return (printRes < 0 ? EXIT_FAILURE : EXIT_SUCCESS);
}

Yes, that is somehow meaningful, you can do that.

But what will you do when malloc(12) fails? Print an error? If malloc(12) failed because the system is out of memory, do you think printf() will work? Can printf() work without using malloc()? Well, the answer is, you don't know it and it probably depends what you print but it might as well depend on a memory allocation and that will fail as well. Honestly, what meaningful error handling can you perform if your system is so much out of memory that you cannot even allocate another 12 bytes?

So what would you do? Just call exit(EXIT_FAILURE)? Is that better than actually crashing because you are accessing the NULL result of malloc(12)? Yes, crashing is not nice but at least you will know where the program crashed doing what exactly if a crashlog is written, you know nothing about it if you had just exited. If you had exited, nobody will ever know what went wrong, nobody can ever fix that problem.

What if malloc(100 * 1024 * 1024) fails? Can you still do error handling? Can you tell the user, that the system is out of memory? Well, most likely you can. You won't need another 100 MB of memory to do that and most likely the system is not completely out of memory, it just cannot provide another 100 MB. At least you should give it a try.

So you see, despite the fact that both examples used malloc(), it does not depend on the function that failed, it depends on the question whether you can handle that error in a meaningful way.

What shall you do if you cannot handle an error in a meaningful way? Well, this depends on another question:

What is your app doing?

If your app deals with data, you probably don't want data to be lost. So if there is data in flight, which may still be saved and saving is still likely to succeed, you should at least try to save it before you exit. But wait! If you need to ensure data in flight is saved in case of an error, you just did handle it in a meaningful way! So you were lying, the answer to question one was "Yes" and thus you should handle that error. As whenever you can handle an error in a meaningful way, you should do exactly that!

So it does not just depend on the function that fails, it also depends on the purpose of your app. In some cases, you even want to catch and handle malloc(12) failing, as you can still do something meaningful then, in other cases that is pointless and why would you do pointless things. On the other hand, you could also ensure that all data is saved when the program exits or when the program crashes, using an atexit or signal handler. In which case it would make no sense again to catch that for every tiny allocation. Yet if your app doesn't even deal with storeable data, e.g. it just fetches something from the network and displays it to the screen, crashing because of a memory failure will have no problematic consequences, other than that your program stops running which it would have to do anyway in case of such an error. So again, it depends.

There is just one thing you should not do: Check for errors that can only appear if external functions work incorrectly. External functions should work as documented. If they don't do, that's a bug and the function is broken but if you cannot even rely on that and even have to test that functions you call are working correctly, what's next? Testing the compiler or the CPU is calculating correctly?

int x = 1 + 1; assert(x == 2);

Seriously? You cannot write programs at all if you cannot rely on certain functionality. If you know for sure that you feed valid input into a function which should never make this function report an error, don't test for an error as this function is not supposed to report one!

Mecki
  • 2,390
  • 1
  • 16
  • 20
3

A bit of an abstract take on the question. And it's not necessarily for the C language.

For larger programs you would have an abstraction layer; perhaps a part of the engine, a library or within a framework. That layer would not care about weather you get a valid data or the output would be some default value: 0, -1, null etc.

Then there's a layer that would be your interface to the abstract layer, that would do a lot of error handling and perhaps other things like dependency injections, event listening etc.

And later you would have your concrete implementation layer where you actually set the rules and handle the output.

So my take on this is that it sometimes is better to completely exclude error handling from a part of code because that part simply doesn't do that job. And then have some processor that would evaluate the output and point to an error.

This is mostly done to separate responsibilities that leads to code readability, and better scalability.

1

In general, unless you have a good reason for not checking that error condition, you should. The only good reason I can think of for not checking for an error condition is when you can't possibly do something meaningful if it fails. This is a very hard bar to meet though, because there's always one viable option: exit gracefully.

1

For most developers, the answer is no. But it does depend on the kind of development you're doing.

I often see junior programmers testing for every error condition here there and everywhere, checking for null pointers passed and exiting early from the function if they are, or returning zero, or some other useless nonsense. I assume this is because it's still something emphasized in coding tests.

What you should be doing is ASSERTING, not TESTING AT RUNTIME, if the error condition is something that should never happen in your code. In the majority of the codebase you, the programmer/team, are in control of the legal state space of operations. If that assert trips during development and testing, find the cause and fix it immediately.

If, during the final weeks of development, you have a handful of assertions that STILL keep tripping despite your best efforts, you should consider adding a runtime test/guard for the error condition, and handling it. This amounts to an admission that you do not fully understand how your program runs and how it got into this state, but occasionally that does happen, even for competent teams, if the state space is sufficiently complex. But you should absolutely NOT be handling these kinds of errors up until this point of admitting defeat.

Now, things change if you have some mission-critical software that is already live and where you can't perform adequate testing offline. In that situation, ideally you would have some proper exception-handling system anyway, and you're architecting from the outset around uncertainty and around keeping running at all costs.

But if you're doing something like game development in a closed, knowable system that is fully under your control, this kind of programming philosophy is completely ill-suited to the problem domain. It clutters and bloats the code, and it makes it hard to reason about the logic. It's far better to specify contracts on methods that assert the legal range of states for parameters and data members, and which terminate execution if they ever fail. If you're getting an erroneous parameter passed through, or the object ever gets into an illegal state, it's not your job to handle that situation; it's your job to fix the logic such that it never happens again.

Kaitain
  • 111
  • 2