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

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:

In either case, the value and error need to be separated.

In C, errors are most commonly represented using the following types:

#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;
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;
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: