Unlike modern languages with exceptions, C has no built-in error handling mechanism. Instead, it relies on return values, global variables, and programmer discipline. This makes error handling in C both flexible and error-prone. Understanding how to properly handle errors is essential for writing robust, reliable C programs that can gracefully handle unexpected situations.
Why Error Handling Matters
Error handling is critical because:
- Hardware can fail (disk full, network down)
- User input can be invalid (wrong format, out of range)
- Resources can be unavailable (memory allocation fails)
- External conditions change (file deleted, permissions changed)
- Bugs exist (null pointers, buffer overflows)
Without proper error handling, programs crash, corrupt data, or create security vulnerabilities.
Common Error Handling Techniques
C provides several approaches to error handling:
| Technique | Description | Use Case |
|---|---|---|
| Return values | Function returns status code | Most common approach |
errno global | Set on system call failures | System-level errors |
setjmp/longjmp | Non-local jumps | Exception-like handling |
| Assertions | Debugging aid | Development only |
| Signal handlers | Asynchronous events | OS signals |
1. Return Value Error Handling
The most common approach is to use return values to indicate success or failure.
#include <stdio.h>
#include <stdlib.h>
// Return 1 for success, 0 for failure
int divide(int a, int b, double *result) {
if (b == 0) {
return 0; // Error: division by zero
}
*result = (double)a / b;
return 1; // Success
}
int main() {
int x = 10, y = 0;
double result;
if (divide(x, y, &result)) {
printf("%d / %d = %f\n", x, y, result);
} else {
printf("Error: Division by zero!\n");
}
return 0;
}
Using Enum for Error Codes:
#include <stdio.h>
typedef enum {
SUCCESS = 0,
ERR_DIVISION_BY_ZERO,
ERR_NEGATIVE_VALUE,
ERR_OUT_OF_RANGE,
ERR_NULL_POINTER
} ErrorCode;
ErrorCode calculateSquareRoot(double value, double *result) {
if (result == NULL) {
return ERR_NULL_POINTER;
}
if (value < 0) {
return ERR_NEGATIVE_VALUE;
}
*result = sqrt(value);
return SUCCESS;
}
const char* errorToString(ErrorCode code) {
switch (code) {
case SUCCESS: return "Success";
case ERR_DIVISION_BY_ZERO: return "Division by zero";
case ERR_NEGATIVE_VALUE: return "Negative value not allowed";
case ERR_OUT_OF_RANGE: return "Value out of range";
case ERR_NULL_POINTER: return "Null pointer provided";
default: return "Unknown error";
}
}
int main() {
double value = -5.0;
double result;
ErrorCode err = calculateSquareRoot(value, &result);
if (err == SUCCESS) {
printf("Square root of %f = %f\n", value, result);
} else {
printf("Error: %s\n", errorToString(err));
}
return 0;
}
2. Using errno for System Errors
The global variable errno (defined in <errno.h>) is set by system calls and library functions on error.
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
int main() {
FILE *file = fopen("nonexistent.txt", "r");
if (file == NULL) {
// errno is set by fopen
printf("Error opening file: %s\n", strerror(errno));
// Or print the error number
printf("errno = %d\n", errno);
// Perror is a convenience function
perror("fopen");
}
// Another example: invalid number conversion
char *str = "123abc";
char *endptr;
errno = 0; // Clear errno before call
long val = strtol(str, &endptr, 10);
if (errno != 0) {
perror("strtol");
} else if (endptr == str) {
printf("No digits found\n");
} else if (*endptr != '\0') {
printf("Partial conversion: %ld, remaining: %s\n", val, endptr);
} else {
printf("Full conversion: %ld\n", val);
}
return 0;
}
Common errno Values:
#include <stdio.h>
#include <errno.h>
int main() {
printf("Common errno values:\n");
printf(" EPERM: %d - Operation not permitted\n", EPERM);
printf(" ENOENT: %d - No such file or directory\n", ENOENT);
printf(" EACCES: %d - Permission denied\n", EACCES);
printf(" EBUSY: %d - Device or resource busy\n", EBUSY);
printf(" ENOMEM: %d - Cannot allocate memory\n", ENOMEM);
printf(" EINVAL: %d - Invalid argument\n", EINVAL);
return 0;
}
3. Resource Cleanup and RAII-like Patterns
Proper error handling must ensure resources are cleaned up:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#define CHECK(cond, cleanup, msg) \
if (!(cond)) { \
perror(msg); \
goto cleanup; \
}
int processFile(const char *filename) {
FILE *file = NULL;
int *buffer = NULL;
int result = -1;
// Open file
file = fopen(filename, "r");
CHECK(file != NULL, cleanup, "Failed to open file");
// Allocate buffer
buffer = malloc(1024);
CHECK(buffer != NULL, cleanup, "Failed to allocate memory");
// Read and process file
size_t bytesRead = fread(buffer, 1, 1024, file);
if (bytesRead == 0 && ferror(file)) {
perror("Failed to read file");
goto cleanup;
}
// Process data...
printf("Successfully read %zu bytes\n", bytesRead);
result = 0; // Success
cleanup:
if (buffer) free(buffer);
if (file) fclose(file);
return result;
}
int main() {
if (processFile("test.txt") == 0) {
printf("File processed successfully\n");
} else {
printf("File processing failed\n");
}
return 0;
}
4. Nested Error Handling with Cleanup
For complex functions with multiple resources:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char *data;
size_t size;
FILE *file;
int *temp_buffer;
} Context;
int initializeContext(Context *ctx) {
memset(ctx, 0, sizeof(Context));
return 0; // Success
}
void cleanupContext(Context *ctx) {
if (ctx->temp_buffer) free(ctx->temp_buffer);
if (ctx->file) fclose(ctx->file);
if (ctx->data) free(ctx->data);
}
int complexOperation(const char *filename) {
Context ctx;
int result = -1;
if (initializeContext(&ctx) != 0) {
return -1;
}
// Allocate data
ctx.data = malloc(1024);
if (!ctx.data) {
perror("Failed to allocate data");
goto cleanup;
}
// Open file
ctx.file = fopen(filename, "r");
if (!ctx.file) {
perror("Failed to open file");
goto cleanup;
}
// Allocate temp buffer
ctx.temp_buffer = malloc(512 * sizeof(int));
if (!ctx.temp_buffer) {
perror("Failed to allocate temp buffer");
goto cleanup;
}
// Read from file
if (fread(ctx.data, 1, 1024, ctx.file) < 1024) {
if (feof(ctx.file)) {
printf("End of file reached\n");
} else if (ferror(ctx.file)) {
perror("Error reading file");
goto cleanup;
}
}
// Process data...
result = 0; // Success
cleanup:
cleanupContext(&ctx);
return result;
}
int main() {
if (complexOperation("test.txt") == 0) {
printf("Operation successful\n");
} else {
printf("Operation failed\n");
}
return 0;
}
5. Setjmp/Longjmp for Exception-like Handling
setjmp and longjmp provide a way to perform non-local jumps, similar to exceptions:
#include <stdio.h>
#include <setjmp.h>
#include <stdlib.h>
jmp_buf exception_buffer;
#define TRY if ((exception_value = setjmp(exception_buffer)) == 0)
#define CATCH(x) else if (exception_value == x)
#define THROW(x) longjmp(exception_buffer, x)
enum Exceptions {
NO_ERROR = 0,
DIVISION_BY_ZERO,
MEMORY_ERROR,
FILE_ERROR
};
int exception_value;
double safe_divide(int a, int b) {
if (b == 0) {
THROW(DIVISION_BY_ZERO);
}
return (double)a / b;
}
void* safe_malloc(size_t size) {
void *ptr = malloc(size);
if (!ptr) {
THROW(MEMORY_ERROR);
}
return ptr;
}
void process_data() {
int *data = safe_malloc(100 * sizeof(int));
double result = safe_divide(10, 0); // Will throw
// This line never executes
free(data);
}
int main() {
TRY {
printf("Trying risky operation...\n");
process_data();
}
CATCH(DIVISION_BY_ZERO) {
printf("Caught division by zero!\n");
}
CATCH(MEMORY_ERROR) {
printf("Caught memory allocation error!\n");
}
printf("Program continues...\n");
return 0;
}
Note: setjmp/longjmp should be used carefully as they can bypass stack unwinding and cause resource leaks.
6. Assertions for Debugging
Assertions are for catching programming errors during development:
#include <stdio.h>
#include <assert.h>
#include <string.h>
// Enable assertions (default)
#define NDEBUG // Comment this out to enable assertions
void processString(const char *str) {
assert(str != NULL); // Crash if str is NULL
assert(strlen(str) > 0); // Crash if empty string
printf("Processing: %s\n", str);
}
int divide(int a, int b) {
assert(b != 0); // Division by zero should never happen in correct code
return a / b;
}
// Custom assertion macro
#define ASSERT_MSG(cond, msg) \
do { \
if (!(cond)) { \
fprintf(stderr, "Assertion failed: %s, file %s, line %d\n", \
msg, __FILE__, __LINE__); \
abort(); \
} \
} while(0)
int main() {
processString("Hello");
// processString(NULL); // Would trigger assertion
printf("10 / 2 = %d\n", divide(10, 2));
// printf("10 / 0 = %d\n", divide(10, 0)); // Would trigger assertion
int x = 5;
ASSERT_MSG(x > 0, "x must be positive");
printf("x = %d\n", x);
return 0;
}
7. Signal Handling for Asynchronous Errors
Signals handle asynchronous events like interrupts or segmentation faults:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
void signal_handler(int sig) {
switch (sig) {
case SIGINT:
printf("\nCaught Ctrl+C. Cleaning up...\n");
// Perform cleanup here
exit(0);
break;
case SIGSEGV:
printf("Segmentation fault! Exiting...\n");
exit(1);
break;
case SIGTERM:
printf("Termination requested. Cleaning up...\n");
exit(0);
break;
}
}
void setup_signal_handlers() {
struct sigaction sa;
sa.sa_handler = signal_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, NULL);
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGSEGV, &sa, NULL);
// Ignore certain signals
signal(SIGPIPE, SIG_IGN);
}
int main() {
setup_signal_handlers();
printf("Program running. Press Ctrl+C to exit.\n");
int counter = 0;
while (1) {
printf("Working... %d\n", counter++);
sleep(1);
}
return 0;
}
8. Error Handling in Libraries
When writing libraries, provide clear error reporting:
#include <stdio.h>
#include <string.h>
#include <errno.h>
// Library error codes
typedef enum {
LIB_SUCCESS = 0,
LIB_ERR_INVALID_PARAM,
LIB_ERR_MEMORY,
LIB_ERR_IO,
LIB_ERR_INTERNAL
} LibError;
// Library context with error tracking
typedef struct {
LibError last_error;
char error_msg[256];
int system_errno;
} LibContext;
void lib_init(LibContext *ctx) {
ctx->last_error = LIB_SUCCESS;
ctx->error_msg[0] = '\0';
ctx->system_errno = 0;
}
void lib_set_error(LibContext *ctx, LibError err, const char *msg) {
ctx->last_error = err;
strncpy(ctx->error_msg, msg, sizeof(ctx->error_msg) - 1);
ctx->system_errno = errno;
}
const char* lib_error_string(LibContext *ctx) {
static char buffer[512];
switch (ctx->last_error) {
case LIB_SUCCESS:
return "Success";
case LIB_ERR_INVALID_PARAM:
snprintf(buffer, sizeof(buffer), "Invalid parameter: %s", ctx->error_msg);
return buffer;
case LIB_ERR_MEMORY:
snprintf(buffer, sizeof(buffer), "Memory error: %s", ctx->error_msg);
return buffer;
case LIB_ERR_IO:
snprintf(buffer, sizeof(buffer), "I/O error: %s (errno: %d - %s)",
ctx->error_msg, ctx->system_errno, strerror(ctx->system_errno));
return buffer;
default:
return "Unknown error";
}
}
// Library function
LibError lib_process_file(LibContext *ctx, const char *filename) {
if (!ctx || !filename) {
lib_set_error(ctx, LIB_ERR_INVALID_PARAM, "Null pointer provided");
return LIB_ERR_INVALID_PARAM;
}
FILE *file = fopen(filename, "r");
if (!file) {
lib_set_error(ctx, LIB_ERR_IO, "Failed to open file");
return LIB_ERR_IO;
}
// Process file...
fclose(file);
ctx->last_error = LIB_SUCCESS;
return LIB_SUCCESS;
}
int main() {
LibContext ctx;
lib_init(&ctx);
LibError err = lib_process_file(&ctx, "nonexistent.txt");
if (err != LIB_SUCCESS) {
printf("Library error: %s\n", lib_error_string(&ctx));
}
return 0;
}
9. Practical Examples
Example 1: Robust File Reader
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
typedef struct {
char *data;
size_t size;
int error_line;
char error_msg[256];
} FileReaderResult;
FileReaderResult* read_file_safe(const char *filename) {
FileReaderResult *result = malloc(sizeof(FileReaderResult));
if (!result) {
return NULL;
}
result->data = NULL;
result->size = 0;
result->error_line = 0;
result->error_msg[0] = '\0';
FILE *file = fopen(filename, "r");
if (!file) {
snprintf(result->error_msg, sizeof(result->error_msg),
"Failed to open file: %s", strerror(errno));
result->error_line = __LINE__;
return result;
}
// Get file size
if (fseek(file, 0, SEEK_END) != 0) {
snprintf(result->error_msg, sizeof(result->error_msg),
"Failed to seek: %s", strerror(errno));
result->error_line = __LINE__;
fclose(file);
return result;
}
long file_size = ftell(file);
if (file_size < 0) {
snprintf(result->error_msg, sizeof(result->error_msg),
"Failed to get file size: %s", strerror(errno));
result->error_line = __LINE__;
fclose(file);
return result;
}
rewind(file);
// Allocate buffer
result->data = malloc(file_size + 1);
if (!result->data) {
snprintf(result->error_msg, sizeof(result->error_msg),
"Failed to allocate memory");
result->error_line = __LINE__;
fclose(file);
return result;
}
// Read file
size_t bytes_read = fread(result->data, 1, file_size, file);
if (bytes_read != file_size && ferror(file)) {
snprintf(result->error_msg, sizeof(result->error_msg),
"Failed to read file: %s", strerror(errno));
result->error_line = __LINE__;
free(result->data);
result->data = NULL;
fclose(file);
return result;
}
result->data[bytes_read] = '\0';
result->size = bytes_read;
fclose(file);
return result;
}
void free_file_result(FileReaderResult *result) {
if (result) {
free(result->data);
free(result);
}
}
int main() {
const char *filename = "test.txt";
FileReaderResult *result = read_file_safe(filename);
if (!result) {
printf("Critical error: Out of memory\n");
return 1;
}
if (result->error_msg[0] != '\0') {
printf("Error reading file at line %d: %s\n",
result->error_line, result->error_msg);
} else {
printf("Successfully read %zu bytes from %s\n",
result->size, filename);
printf("Content: %s\n", result->data);
}
free_file_result(result);
return 0;
}
Example 2: Configuration Parser with Error Reporting
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
typedef struct {
char key[64];
char value[256];
} ConfigEntry;
typedef struct {
ConfigEntry *entries;
int count;
int capacity;
char error_msg[256];
int error_line;
} Config;
Config* config_create() {
Config *cfg = malloc(sizeof(Config));
if (!cfg) return NULL;
cfg->entries = NULL;
cfg->count = 0;
cfg->capacity = 0;
cfg->error_msg[0] = '\0';
cfg->error_line = 0;
return cfg;
}
void config_free(Config *cfg) {
if (cfg) {
free(cfg->entries);
free(cfg);
}
}
int config_set_error(Config *cfg, const char *msg, int line) {
if (cfg) {
strncpy(cfg->error_msg, msg, sizeof(cfg->error_msg) - 1);
cfg->error_line = line;
}
return -1;
}
int config_add_entry(Config *cfg, const char *key, const char *value) {
if (!cfg || !key || !value) {
return config_set_error(cfg, "Null parameter", __LINE__);
}
// Validate key (only letters, numbers, underscore)
for (const char *p = key; *p; p++) {
if (!isalnum(*p) && *p != '_') {
return config_set_error(cfg, "Invalid character in key", __LINE__);
}
}
// Check capacity
if (cfg->count >= cfg->capacity) {
int new_capacity = cfg->capacity == 0 ? 10 : cfg->capacity * 2;
ConfigEntry *new_entries = realloc(cfg->entries,
new_capacity * sizeof(ConfigEntry));
if (!new_entries) {
return config_set_error(cfg, "Out of memory", __LINE__);
}
cfg->entries = new_entries;
cfg->capacity = new_capacity;
}
// Add entry
strncpy(cfg->entries[cfg->count].key, key, sizeof(cfg->entries[0].key) - 1);
strncpy(cfg->entries[cfg->count].value, value, sizeof(cfg->entries[0].value) - 1);
cfg->count++;
return 0; // Success
}
int config_load_file(Config *cfg, const char *filename) {
if (!cfg || !filename) {
return config_set_error(cfg, "Null parameter", __LINE__);
}
FILE *file = fopen(filename, "r");
if (!file) {
return config_set_error(cfg, "Failed to open file", __LINE__);
}
char line[256];
int line_num = 0;
while (fgets(line, sizeof(line), file)) {
line_num++;
// Skip empty lines and comments
char *p = line;
while (isspace(*p)) p++;
if (*p == '\0' || *p == '#') continue;
// Parse key = value
char key[64] = {0};
char value[256] = {0};
if (sscanf(line, "%63[^=] = %255[^\n]", key, value) != 2) {
snprintf(cfg->error_msg, sizeof(cfg->error_msg),
"Invalid syntax at line %d", line_num);
cfg->error_line = __LINE__;
fclose(file);
return -1;
}
// Trim whitespace from key and value
char *key_end = key + strlen(key) - 1;
while (key_end > key && isspace(*key_end)) *key_end-- = '\0';
char *val_start = value;
while (isspace(*val_start)) val_start++;
if (config_add_entry(cfg, key, val_start) != 0) {
fclose(file);
return -1;
}
}
fclose(file);
return 0; // Success
}
const char* config_get(Config *cfg, const char *key, const char *default_value) {
for (int i = 0; i < cfg->count; i++) {
if (strcmp(cfg->entries[i].key, key) == 0) {
return cfg->entries[i].value;
}
}
return default_value;
}
int main() {
Config *cfg = config_create();
if (!cfg) {
printf("Failed to create config\n");
return 1;
}
if (config_load_file(cfg, "config.txt") != 0) {
printf("Error loading config: %s (at internal line %d)\n",
cfg->error_msg, cfg->error_line);
} else {
printf("Config loaded successfully: %d entries\n", cfg->count);
// Query values
const char *host = config_get(cfg, "host", "localhost");
const char *port = config_get(cfg, "port", "8080");
printf("host = %s\n", host);
printf("port = %s\n", port);
}
config_free(cfg);
return 0;
}
Example 3: Network Operation with Timeout
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netdb.h>
#include <signal.h>
#include <setjmp.h>
jmp_buf timeout_jmp;
int timeout_occurred = 0;
void timeout_handler(int sig) {
timeout_occurred = 1;
longjmp(timeout_jmp, 1);
}
int connect_with_timeout(const char *host, int port, int timeout_sec) {
struct sigaction sa;
struct addrinfo hints, *res, *p;
char port_str[16];
int sockfd = -1;
// Setup signal handler for timeout
sa.sa_handler = timeout_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGALRM, &sa, NULL);
// Get address info
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
snprintf(port_str, sizeof(port_str), "%d", port);
int status = getaddrinfo(host, port_str, &hints, &res);
if (status != 0) {
fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status));
return -1;
}
// Try each address
for (p = res; p != NULL; p = p->ai_next) {
sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
if (sockfd < 0) continue;
// Set timeout using setjmp/longjmp
timeout_occurred = 0;
if (setjmp(timeout_jmp) == 0) {
alarm(timeout_sec);
if (connect(sockfd, p->ai_addr, p->ai_addrlen) == 0) {
alarm(0); // Cancel alarm
break; // Success
}
alarm(0); // Cancel alarm on failure
}
if (timeout_occurred) {
fprintf(stderr, "Connection timeout after %d seconds\n", timeout_sec);
close(sockfd);
sockfd = -1;
break;
}
close(sockfd);
sockfd = -1;
}
freeaddrinfo(res);
if (sockfd < 0 && !timeout_occurred) {
fprintf(stderr, "Failed to connect: %s\n", strerror(errno));
}
return sockfd;
}
int main() {
int sock = connect_with_timeout("example.com", 80, 5);
if (sock >= 0) {
printf("Connected successfully!\n");
// Send HTTP request
const char *request = "GET / HTTP/1.0\r\nHost: example.com\r\n\r\n";
send(sock, request, strlen(request), 0);
// Receive response
char buffer[4096];
int bytes = recv(sock, buffer, sizeof(buffer) - 1, 0);
if (bytes > 0) {
buffer[bytes] = '\0';
printf("Received %d bytes\n", bytes);
}
close(sock);
} else {
printf("Connection failed\n");
}
return 0;
}
10. Error Handling Best Practices
1. Check All Return Values
// WRONG
fopen("file.txt", "r");
fread(buffer, 1, 100, file);
// CORRECT
FILE *file = fopen("file.txt", "r");
if (file == NULL) {
// handle error
}
size_t bytes = fread(buffer, 1, 100, file);
if (bytes < 100 && ferror(file)) {
// handle error
}
2. Clean Up Resources on Error
void* ptr1 = NULL;
void* ptr2 = NULL;
FILE* file = NULL;
ptr1 = malloc(100);
if (!ptr1) goto cleanup;
ptr2 = malloc(200);
if (!ptr2) goto cleanup;
file = fopen("file.txt", "r");
if (!file) goto cleanup;
// Success - use resources
cleanup:
if (file) fclose(file);
free(ptr2);
free(ptr1);
if (error) return -1;
3. Provide Meaningful Error Messages
// BAD
printf("Error!\n");
// GOOD
fprintf(stderr, "Failed to open config file '%s': %s\n",
filename, strerror(errno));
4. Use Consistent Error Handling Patterns
// Define a consistent error handling macro
#define CHECK(cond, msg) \
if (!(cond)) { \
fprintf(stderr, "Error: %s at %s:%d\n", msg, __FILE__, __LINE__); \
goto error; \
}
// Use it consistently
CHECK(ptr != NULL, "Null pointer");
CHECK(file != NULL, "File open failed");
5. Consider Error Recovery Strategies
- Retry: For transient errors (network timeouts)
- Fallback: Use default values when config missing
- Graceful degradation: Disable features that depend on failed resources
- User notification: Inform user of errors and possible actions
Common Pitfalls
| Mistake | Consequence | Solution |
|---|---|---|
| Ignoring return values | Silent failures | Check all return values |
Not checking errno immediately | Wrong error reported | Check errno right after failure |
| Forgetting to free resources | Memory leaks | Use cleanup labels |
Assuming malloc always succeeds | Crash on NULL | Always check allocation |
| Not validating input | Security vulnerabilities | Validate all input |
| Overlooking partial reads/writes | Data corruption | Check actual bytes transferred |
Error Handling Strategy Decision Tree
┌─────────────────┐ │ Error Occurs │ └────────┬────────┘ │ ┌──────────────┴──────────────┐ │ │ Can recover? Cannot recover? │ │ ┌────┴────┐ ┌────┴────┐ │ │ │ │ Retry? Fallback? Log and Abort with │ │ continue message ┌────┴────┐ │ │ │ │ │ │ │ │ Yes───┐ No──┐│ ┌─────┘ │ │ │ │ ││ │ │ Retry Log Use ───────────┘ │ and and default │ retry continue value │ with │ reduced │ function │ │ Inform user and exit
Conclusion
Error handling in C requires discipline and careful planning. Unlike languages with exceptions, C forces you to be explicit about error conditions and their handling. This can make code more verbose but also more predictable.
Key principles for effective error handling:
- Always check return values - Never assume operations succeed
- Clean up resources - Use
gotoor nested ifs for cleanup - Provide context - Include file, line, and reason in error messages
- Be consistent - Use the same error handling pattern throughout
- Consider recovery - Think about whether and how to recover
- Validate input - Check all external data before use
- Test error paths - Ensure error handling code itself is correct
Mastering error handling in C is what separates production-quality code from quick prototypes. It's the difference between a program that crashes mysteriously and one that gracefully handles unexpected situations, providing useful feedback to users and developers alike.