Handling errors in C

In any program, things will inevitably go wrong at some point. Error handling is provided by the standard library in C as well, albeit not as streamlined as other languages.

POSIX error codes

The most reliable error mechanism is defined in the errno.h header of the C standard library.


Functions will typically signal that something went wrong by returning NULL or negative values, but that does not tell you what went wrong. The distinction matters because some errors are recoverable while others are not. To receive more information about the cause of errors, the errno variable in errno.h is set to specific values when encountering errors in standard library functions. Note that errno is only safe per-thread, not across threads


Theoretically, only three error values are actually required by the C standard, but almost all implementations provide the POSIX / GNU libc extensions as well, adding over 100 additional ones for common errors.


Error values are defined as compiler macros in errno.h, written in uppercase and starting with E, like EBADF (bad file descriptor) or ENOENT (no such file or directory). The errno.h error handling approach is the only way to handle errors across platforms, with complete POSIX support on Linux, BSD and Mac, and a majority of error values present on Windows as well (although not all).


You use errno.h by checking the errno value against known macro error constants directly after receiving a failure signal:

#include <stdio.h>
#include <errno.h>
#include <fcntl.h>

int main(){
  int fd = open("bad_file.txt", O_RDONLY);
  if(fd == -1){
    switch(errno){
      case EACCES:
        // permission error
        break;
      case ENOENT:
        // file does not exist
        break;
      default:
        // other error
    }
  }
  return 0;
}

By checking errno directly after the failed function call, you can check if it matches any of the errors that function might return. You need to know the exact macro constant name of the error you are trying to handle, refer to the function documentation for a complete list.


If you do not need to run custom logic depending on error and instead just need a human-readable description of the error, you can use the strerror function from the string.h header:

#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>

int main(){
  int fd = open("bad_file.txt", O_RDONLY);
  if(fd == -1){
    printf("Error: %s", strerror(errno));
    return 1;
  }
  return 0;
}

The strerror function returns the official error descriptions for the error value, for example No such file or directory for ENOENT. Note that strerror is not thread-safe, use strerror_r or strerror_s for multithreaded programs.

See the Wikipedia errno.h page or run man 3 errno on a linux machine for a full list of POSIX error codes.


Functions that cannot indicate errors through return values require developers to manually zero errno before use, for example strtol from stdlib.h:

errno = 0; // MUST clear errno
const char *str = "ABC";
char *end;
long val = strtol(str, &end, 10);

if (str == end){
  // no conversion
}else if(errno != 0){
  // conversion error
}

Since strtol always returns a long and any value could theoretically be the result of a successful conversion, it relies entirely on errno to signal conversion failure. If errno is still set to a non-zero value by a previous error, you cannot distinguish between that and strtol erroring, because strtol does not set errno to 0 on success, only to error values on failure.

Standard error helper functions

The most common use of error codes is using to provide debugging output in some form. To that end, the standard library includes two convenience functions.


The first one is perror from the stdio.h header, which prints a custom message and a human-readable description of the latest errno value to the stderr output stream:

#include <stdio.h>
#include <stdlib.h>

int main(){
  char *data = malloc(999999999999999999);
  if(!data){
    perror("Could not prepare *data");
    return 1;
  }
  return 0;
}

On most platforms, the amount of memory is too much to allocate at once, resulting in the error message:

Could not prepare *data: Cannot allocate memory

As you can see, the custom message is automatically succeeded by a human-readable errno description of the last recorded error.


If you want more control over the program output but still work with human-readable error messages, you can use strerror from the string.h header instead:

#include <stdio.h>
#include <string.h>
#include <errno.h>

int main(){
  char *data = malloc(999999999999999999);
  if(!data){
    printf("Error description: %s\n", strerror(errno));
    return 1;
  }
  return 0;
}

This will print

Error description: Cannot allocate memory

Growing codebases often choose to build soft logging wrappers using strerror for more control:

#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void log(const char *log_level, const char *msg, bool show_errno){
  fprintf(stderr, "%s: %s", log_level, msg);
  if(show_errno){
    fprintf(stderr, ": %s", strerror(errno));
  }
  fprintf(stderr, "\n");
}
// logs warning
void warn(const char *msg, bool show_errno){
  log("WARNING", msg, show_errno);
}
// logs error
void error(const char *msg, bool show_errno){
  log("ERROR", msg, show_errno);
}
// logs error and exits
void fatal(const char *msg, bool show_errno){
  error(msg, show_errno);
  exit(1);
}

The main advantage of such simple wrappers is their compatibility across operating systems and platforms, making them a good default choice for error handling shortcuts.

BSD error wrappers

Initially developed by BSD, the err.h header is now commonly available across BSD; Mac and Linux systems. It offers a warning and an error handler, with alternatives for automatically appended errno messages and support for variadic arguments.

The simple form append errno values to a custom message and support printf style message formatting:

// prints "<program name>: My warning Message": <errno description>
warn("My %s message", "warning");
// same warning but without formatting:
warn("My warning message");

// prints "<program name>: My message: <errno description>"
// and exits with status 1
err(1, "My %s message", "error");

Note that err is intended for unrecoverable errors and will always stop the program, which is why it takes an integer status as the first argument. Be careful to pass a non-zero value here, as most operating systems expect a program to exit with a return code larger than zero to indicate errors.


If you want only the message without errno, append an x to the function names:

// prints "<program name>: My Message"
warnx("My message");

// prints "<program name>: My message"
// and exits with status 1
errx(1, "My message");

Both warn and err variants have virtual alternatives prefixed with v if you need to pass in variadic arguments through the standard stdarg.h mechanism:

#include <stdarg.h>
#include <err.h>

void fatalerr(const char *fmt, ...){
  va_list ap;
  va_start(ap, fmt);
  verr(1, fmt, ap);
  va_end(ap);
}

The main benefit of these functions is when writing custom wrappers as you can see in the example above.

Linux-native error wrappers

The error.h header is available on linux, and sometimes on BSD variants. It provides only two functions, the simpler one being error:

#include <error.h>
#include <errno.h>

int main(){
  // if errno is 0, prints "<program name>: My error message
  // else printfs "<program name>: My error message: <errno description>"
  // exits the program with status code 1 in both cases
  error(1, errno, "My %s message", "error");
  return 0;
}

The linux error variant makes status code and errno mandatory arguments, but does not print an errno message if the passed value is 0. This makes it possible for developers to intentionally skip errno by passing a literal 0 value instead of the real errno value.


The second function error_at_line in the error.h header does the same, but takes two more arguments for line number and source file name:

#include <error.h>
#include <errno.h>

int main(){
  // if errno is 0, prints "<program name>:<file>:<line>: My error message
  // else printfs "<program name>:<file>:<line>: My error message: <errno description>"
  // exits the program with status code 1 in both cases
  error_at_line(1, errno, __FILE__, __LINE__, "My %s message", "error");
  return 0;
}

You will almost always use compiler macros __FILE__ and __LINE__ for the source file and line arguments. The linux error handling wrapper is mostly useful for programs that should feel like native GNU tools, as most system programs like coreutils use the same error handling.

Windows errors

Windows is the only platform that does not reliably use errno, although providing it and some POSIX error codes. Instead, the preferred way is to use the windows.h header and use GetLastError and FormatMessage for error handling:

#include <windows.h>

void PrintLastError(void){
  DWORD err = GetLastError();
  char *msg = NULL;
  FormatMessage(
    FORMAT_MESSAGE_ALLOCATE_BUFFER | 
    FORMAT_MESSAGE_FROM_SYSTEM | 
    FORMAT_MESSAGE_IGNORE_INSERTS, // formatting settings
    NULL, // message source
    err, // error id/code
    0, // language id
    (LPSTR)&msg, // buffer to write formatted msg to
    0, // buffer size (if providing buffer)
    NULL // arguments for inserts
  );
  fprintf(stderr, "[error %lu] %s\n", err, msg);
  LocalFree(msg);
}

Windows error handling is fairly verbose, so writing helper functions like above is common. Note that FormatMessage may use either ASCII or UTF-16 text encoding, depending on platform. Use FormatMessageA for ASCII or FormatMessageW for UTF-16 respectively.

More articles

Go unit testing essentials

Real world testing patterns and workarounds

Finding unwanted unicode characters in source code files

Preventing hidden and lookalike characters from breaking yaml and python