Error handling
Some thoughts about error handling in software development.
Aims
Code consists of chunks of code (functions, member methods ...)- If a function is executed, and an error occurs, the function should reset the state of the program to the state it was in when the function started executing
- li Error handling should be separated from ops code as well as possible
- An error should be easily dectectable and its cause be obviously from the logs. This includes spawning just enough - but not more - error messages
General
There are various patterns and features in recent languages that address the problem of error handling. However, in many cases you are forced to rely on the most basic ones, e.g. in case you deal with a language that does not provide for special features like C, or you are forced to interact with an API that does not support them, like an operation system API. Ultimately, all error handling at its core relies on few simple patterns:- keeping the error local to the function
- propagating the error via some kind of return value
- or a combination of these
if you got code like:
bool get_data(char const *path, int *recv_buffer, size_t num_entries) {
int fh = open(path, O_READ);
if(0 > fh) {
return false;
}
const size_t bytes_to_read = num_entries * sizeof(int);
size_t bytes_read = read(fh, recv_buffer, bytes_to_read);
close(fh);
if(bytes_to_read != bytes_read) {
return E_NOT_ENOUGH_DATA_FOUND;
}
return true;
}
...
int recv_buffer[255];
if(! get_data("data.dat", reccv_buffer, 255)) {
fprintf(stderr, "Could not retrieve data from file\n");
exit(EXIT_FAIL);
}
for(size_t i = 0; 255 > i; ++i) {
process_int(recv[i]);
}
...
If your program fails to execute and exits with the message Could not retrieve data from file
you might wonder why it was not able to retrieve the data - was the file missing? Weren't there enough data in the file? You could use the return value of get_data
to return more information about the error. This is totally fine if there is a finite set of error causes. Your code might then transform into something like this: ...
void process_ints(int const *buffer, const size_t num_ints) {
for(size_t i = 0; num_ints > i; ++i) {
process_int(buffer[i]);
}
}
...
switch(get_data("data.dat", recv_buffer, 255)) {
case 0:
process_ints(recv_buffer, 255);
break;
case ENOENT:
fprintf(stderr, "No such file or directory");
break;
default:
assert(! "MUST NEVER HAPPEN");
};
...
(Always remember to make your switches fail fast in case of unexpected values - always add an appropriate default:
branch! ) Note that we don't want to define ever new error codes - there are plenty already defined for various situations in C / POSIX, and probably some that suit your needs. It standardises the code and also simplifies writing your code. If you implement get_data
without using predefined error codes, a portion of it would look like int get_data(char const *path, int *recv_buffer, size_t num_entries) {
fh = open(path, O_RDONLY);
if(0 > fh) {
return E_FILE_NOT_FOUND;
}
const size_t bytes_to_read = num_entries * sizeof(int);
size_t bytes_read = read(fh, recv_buffer, bytes_to_read);
close(fh);
if(bytes_to_read != bytes_read) {
return E_NOT_ENOUGH_DATA_FOUND;
}
return 0;
}
...
switch(get_data("data.dat", recv_buffer, 255)) {
case 0:
process_ints(recv_buffer, 255);
break;
case E_FILE_NOT_FOUND:
fprintf(stderr, "No such file or directory");
break;
case E_NOT_ENOUGH_DATA:
fprintf(stderr, "Not enough data in file\n");
break;
default:
assert(! "MUST NEVER HAPPEN");
};
...
Apart from the fact that somewhere in your code you need to define these additional constants, bloating your code, you also have to add an extra translation layer - and your error code might be misleading: What if the file exists, but is a directory? If you instead rely on the predefined error codes, you might just implement the same portion of code like int get_data(char const *path, int *recv_buffer, size_t num_entries) {
fh = open(path, O_RDONLY);
if(0 > fh) {
return errno;
}
const size_t bytes_to_read = num_entries * sizeof(int);
size_t bytes_read = read(fh, recv_buffer, bytes_to_read);
close(fh);
if(bytes_to_read != bytes_read) {
return ENODATA;
}
return 0;
}
...
int retval = get_data("data.dat", recv_buffer, 255);
if(0 > retval) {
fprintf(stderr, "Could not retrieve data: %s\n", strerror(retval));
}
...
The standardised codes are well supported: - You do not have to define the error constants
- You have various tools handy to deal with them (like the strerror(3) function, or the errno(1) command line utiliy)
- You don't surprise the users of your code
The value of avoiding confusing / surprising the users of your code cannot be underestimated and is often referred to as 'principle of least surprise'. You could even redesign the
get_data
to most closely resemle that of read(2)
and return the number of bytes read in case of success: int get_data(char const *path, int *recv_buffer, size_t int_to_read) {
fh = open(path, O_RDONLY);
if(0 > fh) {
return errno;
}
const size_t bytes_to_read = num_entries * sizeof(int);
size_t bytes_read = read(fh, recv_buffer, bytes_to_read);
close(fh);
if(bytes_to_read != bytes_read) {
return ENODATA;
}
return bytes_read / 4;
}
However, the best approach is to just keep the errror local to the function it appears in. For instance, get_data
could be implemented like size_t get_data(char const *path, int *recv, size_t recv_size_ints) {
fh = open(path, O_RDONLY);
if(0 > fh) {
fprintf("Could not open file %s: %s", path, strerror(errno));
return 0;
}
const size_t bytes_to_read = recv_size_ints * sizeof(int);
size_t bytes_read = read(fh, recv_buffer, bytes_to_read);
close(fh);
if(0 > bytes_read) {
fprintf(stderr, "Could not read from file %s: %s\n", path,
strerror(errno));
return 0;
}
if(0 == bytes_read) {
fprintf(stderr, "No data received\n");
}
return bytes_read / 4;
}
...
int recv[255] = {0};
const size_t num_ints = get_data("data.dat", recv, 255);
for(size_t i = 0; num_ints > i; ++i) {
process_int(recv[i]);
}
...
This way, the caller needs not even bother whether an error occured in the lower layer, because it is contained within the lower layer.