Error handling techniques in C
08-11-2026
In C, there is no standard way for a function to return an error to its caller. Instead, different C projects use different error handling techniques and their variations.
- Return sentinel value on failure
- Return value, set error through thread local variable (C11)
- Return error, set value through output parameter
- Return value, set error through output parameter
- Return value, store error in context
- Return error and value using a struct (C99, C11)
- Return error and value using a struct (C23, C2y)
Return sentinel value on failure
If a function should return true for success, it can return false to indicate failure.
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
static bool do_something(void);
int main(void)
{
if (do_something() == false) {
fprintf(stderr, "do_something() failed");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
static bool do_something(void)
{
return false;
}
If a function should return a positive integer, such as a count, it can return -1 to indicate failure.
#include <stdlib.h>
#include <stdio.h>
static int do_something(void);
int main(void)
{
int count = 0;
count = do_something();
if (count == -1) {
fprintf(stderr, "do_something() failed");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
static int do_something(void)
{
return -1;
}
If a function should return a pointer, it can return NULL to indicate failure.
#include <stdlib.h>
#include <stdio.h>
static void *do_something(void);
int main(void)
{
char *ptr = NULL;
ptr = do_something();
if (ptr == NULL) {
fprintf(stderr, "do_something() failed");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
static void *do_something(void)
{
return NULL;
}
This works until one or both of the following happens:
- The function has multiple failure points, and the caller needs more details about the error.
- The return value uses its entire range to represent valid results, leaving no sentinel values available to indicate failure.
In either case, the value and error need to be separated.
In C, errors are most commonly represented using the following types:
intwith a function to translate the error code into a message.
#define ERROR1 1
#define ERROR2 2
const char *error_message_get(int error)
{
switch (error) {
case ERROR1:
return "ERROR1 message";
case ERROR2:
return "ERROR2 message";
default:
return "Unknown error";
}
}
int error = 0;
enumwith a function to translate the error code into a message.
typedef enum {
ERROR1 = 1,
ERROR2 = 2,
} error_t;
const char *error_message_get(error_t error)
{
switch (error) {
case ERROR1:
return "ERROR1 message";
case ERROR2:
return "ERROR2 message";
default:
return "Unknown error";
}
}
error_t error = 0;
structcontaining an error code, a message, and potentially other error related information.
typedef struct error {
enum {
ERROR1 = 1,
ERROR2 = 2,
} code;
const char *message;
} error_t;
error_t error = {0};
Return value, set error through thread local variable (C11)
#include <stdlib.h>
#include <stdio.h>
#include <limits.h>
#define EDIVZERO 1
#define EOVERFLOW 2
static int divide(int a, int b);
static _Thread_local int error = 0;
int main(void)
{
int value = 0;
value = divide(10, 0);
if (error) {
fprintf(stderr, "divide error: %d\n", error);
return EXIT_FAILURE;
}
printf("divide result: %d\n", value);
return EXIT_SUCCESS;
}
static int divide(int a, int b)
{
if (b == 0) {
error = EDIVZERO;
return 0;
}
if (a == INT_MIN && b == -1) {
error = EOVERFLOW;
return 0;
}
return a / b;
}
Projects that use this technique:
Return error, set value through output parameter
#include <stdlib.h>
#include <stdio.h>
#include <limits.h>
#define EDIVZERO 1
#define EOVERFLOW 2
static int divide(int a, int b, int *output);
int main(void)
{
int error = 0;
int value = 0;
error = divide(10, 0, &value);
if (error) {
fprintf(stderr, "divide error: %d\n", error);
return EXIT_FAILURE;
}
printf("divide result: %d\n", value);
return EXIT_SUCCESS;
}
static int divide(int a, int b, int *output)
{
if (b == 0) {
return EDIVZERO;
}
if (a == INT_MIN && b == -1) {
return EOVERFLOW;
}
*output = a / b;
return 0;
}
Projects that use this technique:
Return value, set error through output parameter
#include <stdlib.h>
#include <stdio.h>
#include <limits.h>
#define EDIVZERO 1
#define EOVERFLOW 2
static int divide(int a, int b, int *error);
int main(void)
{
int error = 0;
int value = 0;
value = divide(10, 0, &error);
if (error) {
fprintf(stderr, "divide() error: %d\n", error);
return EXIT_FAILURE;
}
printf("divide result: %d\n", value);
return EXIT_SUCCESS;
}
static int divide(int a, int b, int *error)
{
if (b == 0) {
*error = EDIVZERO;
return 0;
}
if (a == INT_MIN && b == -1) {
*error = EOVERFLOW;
return 0;
}
*error = 0;
return a / b;
}
Projects that use this technique:
This technique can also be used to implement the sticky error pattern, allowing the caller to check for an error only after the final function call.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define EOVERFLOW 1
#define EWRITE 2
static void string_append(char *string, size_t string_size, const char *suffix, int *error);
static void string_write(const char *string, FILE *stream, int *error);
int main(void)
{
int error = 0;
char string[16 + 1] = "";
string_append(string, sizeof(string), "Error ", &error);
string_append(string, sizeof(string), "handling ", &error);
string_append(string, sizeof(string), "techniques ", &error);
string_append(string, sizeof(string), "in C\n", &error);
string_write(string, stdout, &error);
if (error) {
fprintf(stderr, "string operation error: %d\n", error);
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
static void string_append(char *string, size_t string_size, const char *suffix, int *error)
{
size_t string_length = 0;
size_t suffix_length = 0;
if (*error) {
return;
}
string_length = strlen(string);
suffix_length = strlen(suffix);
if (string_length + suffix_length + 1 > string_size) {
*error = EOVERFLOW;
return;
}
memcpy(string + string_length, suffix, suffix_length + 1);
}
static void string_write(const char *string, FILE *stream, int *error)
{
if (*error) {
return;
}
if (fputs(string, stream) == EOF) {
*error = EWRITE;
return;
}
}
Projects that use this technique:
Return value, store error in context
#include <stdio.h>
#include <stdlib.h>
#define EALLOC 1
#define ERANGE 2
typedef struct {
int error;
int *data;
size_t size;
} array_t;
static array_t *array_create(void);
static void array_destroy(array_t *array);
static int array_error_get(array_t *array);
static size_t arrayize_get(array_t *array);
static void array_value_add(array_t *array, int value);
static int array_value_get(array_t *array, size_t index);
int main(void)
{
int error = 0;
int value = 0;
array_t *array = array_create();
if (array == NULL) {
fprintf(stderr, "array_create() failed\n");
return EXIT_FAILURE;
}
array_value_add(array, 10);
array_value_add(array, 20);
array_value_add(array, 30);
// ERANGE error caused by <=
for (size_t i = 0; i <= arrayize_get(array); i++) {
value = array_value_get(array, i);
if ((error = array_error_get(array))) {
fprintf(stderr, "array_error_get() error: %d\n", error);
array_destroy(array);
return EXIT_FAILURE;
}
printf("value at index %zu: %d\n", i, value);
}
array_destroy(array);
return EXIT_SUCCESS;
}
array_t *array_create(void)
{
return calloc(1, sizeof(array_t));
}
void array_destroy(array_t *array)
{
if (array) {
free(array->data);
free(array);
}
}
int array_error_get(array_t *array)
{
return array->error;
}
size_t arrayize_get(array_t *array)
{
array->error = 0;
return array->size;
}
void array_value_add(array_t *array, int value)
{
int *data = NULL;
if ((data = realloc(array->data, (array->size + 1) * sizeof(*data))) == NULL) {
array->error = EALLOC;
return;
}
array->error = 0;
array->data = data;
array->data[array->size++] = value;
}
int array_value_get(array_t *array, size_t index)
{
if (index >= array->size) {
array->error = ERANGE;
return 0;
}
array->error = 0;
return array->data[index];
}
Projects that use this technique:
This technique can also be used to implement the sticky error pattern.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#define EOVERFLOW 1
#define EWRITE 2
typedef struct {
int error;
char *data;
size_t size;
size_t capacity;
} string_t;
static string_t *string_create(size_t capacity);
static void string_destroy(string_t *string);
static int string_error_get(const string_t *string);
static void string_append(string_t *string, const char *suffix);
static void string_write(string_t *string, FILE *stream);
int main(void)
{
int error = 0;
string_t *string = NULL;
string = string_create(16);
if (string == NULL) {
fprintf(stderr, "string_create() failed\n");
return EXIT_FAILURE;
}
string_append(string, "Error ");
string_append(string, "handling ");
string_append(string, "techniques ");
string_append(string, "in C\n");
string_write(string, stdout);
if ((error = string_error_get(string))) {
fprintf(stderr, "string operation error: %d\n", error);
string_destroy(string);
return EXIT_FAILURE;
}
string_destroy(string);
return EXIT_SUCCESS;
}
static string_t *string_create(size_t capacity)
{
string_t *string = NULL;
if ((string = calloc(1, sizeof(*string))) == NULL) {
return NULL;
}
if ((string->data = calloc(1, capacity + 1)) == NULL) {
string_destroy(string);
return NULL;
}
string->error = 0;
string->data[0] = '\0';
string->size = 0;
string->capacity = capacity;
return string;
}
static void string_destroy(string_t *string)
{
if (string) {
free(string->data);
free(string);
}
}
static int string_error_get(const string_t *string)
{
return string->error;
}
static void string_append(string_t *string, const char *suffix)
{
size_t suffix_length = 0;
if (string->error) {
return;
}
suffix_length = strlen(suffix);
if (string->size + suffix_length + 1 > string->capacity + 1) {
string->error = EOVERFLOW;
return;
}
memcpy(string->data + string->size, suffix, suffix_length + 1);
string->size += suffix_length;
}
static void string_write(string_t *string, FILE *stream)
{
if (string->error) {
return;
}
if (fputs(string->data, stream) == EOF) {
string->error = EWRITE;
return;
}
}
Projects that use this technique:
Return error and value using a struct (C99, C11)
#include <stdlib.h>
#include <stdio.h>
#include <limits.h>
#define EDIVZERO 1
#define EOVERFLOW 2
static struct divide_result {int error; int value;} divide(int a, int b);
int main(void)
{
struct divide_result result = divide(10, 0);
if (result.error) {
fprintf(stderr, "divide error: %d\n", result.error);
return EXIT_FAILURE;
}
printf("divide result: %d\n", result.value);
return EXIT_SUCCESS;
}
static struct divide_result divide(int a, int b)
{
if (b == 0) {
return (struct divide_result) {.error = EDIVZERO};
}
if (a == INT_MIN && b == -1) {
return (struct divide_result) {.error = EOVERFLOW};
}
return (struct divide_result) {.value = a / b};
}
Typedef version
#include <stdlib.h>
#include <stdio.h>
#include <limits.h>
#define EDIVZERO 1
#define EOVERFLOW 2
typedef struct {int error; int value;} divide_result_t;
static divide_result_t divide(int a, int b);
int main(void)
{
divide_result_t result = divide(10, 0);
if (result.error) {
fprintf(stderr, "divide error: %d\n", result.error);
return EXIT_FAILURE;
}
printf("divide result: %d\n", result.value);
return EXIT_SUCCESS;
}
static divide_result_t divide(int a, int b)
{
if (b == 0) {
return (divide_result_t) {.error = EDIVZERO};
}
if (a == INT_MIN && b == -1) {
return (divide_result_t) {.error = EOVERFLOW};
}
return (divide_result_t) {.value = a / b};
}
Tagged union version (C11 anonymous union)
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <limits.h>
#define EDIVZERO 1
#define EOVERFLOW 2
typedef struct {
bool success;
union {
int error;
int value;
};
} divide_result_t;
static divide_result_t divide(int a, int b);
int main(void)
{
divide_result_t result = divide(10, 0);
if (result.success == false) {
fprintf(stderr, "divide error: %d\n", result.error);
return EXIT_FAILURE;
}
printf("divide result: %d\n", result.value);
return EXIT_SUCCESS;
}
static divide_result_t divide(int a, int b)
{
if (b == 0) {
return (divide_result_t) {.error = EDIVZERO};
}
if (a == INT_MIN && b == -1) {
return (divide_result_t) {.error = EOVERFLOW};
}
return (divide_result_t) {.success = true, .value = a / b};
}
Return error and value using a struct (C23, C2y)
C23 adopted N3037 - Improved Rules for Tag Compatibility, allowing redeclaration of the same tagged types.
#include <stdlib.h>
#include <stdio.h>
#include <limits.h>
#define EDIVZERO 1
#define EOVERFLOW 2
#define RESULT(E, V) \
struct result_##E##_##V { \
E error; \
V value; \
}
static RESULT(int, int) divide(int a, int b);
int main(void)
{
RESULT(int, int) result = divide(10, 0);
if (result.error) {
fprintf(stderr, "divide error: %d\n", result.error);
return EXIT_FAILURE;
}
printf("divide result: %d\n", result.value);
return EXIT_SUCCESS;
}
static RESULT(int, int) divide(int a, int b)
{
if (b == 0) {
return (RESULT(int, int)) {.error = EDIVZERO};
}
if (a == INT_MIN && b == -1) {
return (RESULT(int, int)) {.error = EOVERFLOW};
}
return (RESULT(int, int)) {.value = a / b};
}
However, types without tags remain incompatible, so types with multiple tokens must be typedef'd for the RESULT macro to work.
typedef char * charptr;
RESULT(int, charptr)
C2y will hopefully adpot N3744, allowing the RESULT macro to use an anonymous struct and eliminating the need to construct struct tags from type names.
Projects that use this technique: