CoreTechED

Loading

Learn C error handling: errno, perror, strerror, ferror, feof, clearerr, and exit statuses. Master robust error detection and handling.

Error Handling in C: Complete Guide with Examples

Error Handling in C: A Comprehensive Tutorial

Error handling in C is performed using function return values, error codes, and the global errno variable, since C does not provide built-in exception handling like try-catch. Most functions return -1, NULL, or set errno when an error occurs. Understanding error handling is essential for writing robust, reliable programs that gracefully handle failures.

This tutorial provides an in-depth exploration of error handling in C: errno, perror, strerror, ferror, feof, clearerr, exit statuses, manual validation, common pitfalls, and best practices.


Conceptual Introduction & Fundamentals

What is Error Handling?

Error handling is the process of detecting, reporting, and responding to errors that occur during program execution. In C, this is done through:

  • Return values: Functions return special values (NULL, -1, EOF) on error.
  • Error codes: The errno variable stores specific error codes.
  • Error reporting: perror and strerror provide descriptive messages.
  • Exit statuses: Programs communicate success/failure to the OS.

Why Error Handling Matters

  • Prevent crashes: Detect errors before they cause undefined behavior.
  • Inform users: Provide clear error messages.
  • Resource management: Clean up allocated resources on failure.
  • Debugging: Identify the root cause of failures.
  • Robustness: Handle unexpected conditions gracefully.

C vs. Exception-Based Languages

C does not have built-in exception handling (try-catch). Instead, errors are handled through:

  • Checking return values
  • Checking errno
  • Manual validation of conditions
  • Early returns or exit() calls

Syntax & Core Mechanics

errno — Error Number

errno is a global variable defined in <errno.h> that stores the error code when a library function fails.

c

#include <stdio.h> #include <errno.h> int main(void) { FILE *fp = fopen("missing.txt", "r"); if (fp == NULL) { printf("Error Code: %d\n", errno); perror("Error"); } return 0; }

Expected output:

text

Error Code: 2 Error: No such file or directory

Common errno values:

errnoMeaning
1Operation not permitted
2No such file or directory
3No such process
5I/O error
12Out of memory
13Permission denied

perror() — Print Error Message

perror prints a descriptive error message to stderr based on the current errno.

c

#include <stdio.h> #include <errno.h> int main(void) { FILE *fp = fopen("nonexistent.txt", "r"); if (fp == NULL) { perror("Error opening file"); } return 0; }

Expected output:

text

Error opening file: No such file or directory

strerror() — Error String

strerror returns a pointer to the textual description of an error code.

c

#include <stdio.h> #include <string.h> #include <errno.h> int main(void) { FILE *fp = fopen("nonexistent.txt", "r"); if (fp == NULL) { printf("Error: %s\n", strerror(errno)); } return 0; }

Expected output:

text

Error: No such file or directory

ferror() — File Error Check

ferror checks if an error occurred during a file operation.

c

#include <stdio.h> int main(void) { FILE *fp = fopen("test.txt", "w"); if (fp == NULL) { perror("Error opening file"); return 1; } fprintf(fp, "Hello, World!\n"); if (ferror(fp) == 0) { printf("Data written successfully.\n"); } else { printf("Error writing data.\n"); } fclose(fp); return 0; }

Expected output:

text

Data written successfully.

feof() — End of File Check

feof checks whether the end of a file has been reached.

c

#include <stdio.h> int main(void) { FILE *fp = fopen("test.txt", "r"); if (fp == NULL) { perror("Error opening file"); return 1; } int ch; while ((ch = fgetc(fp)) != EOF) { putchar(ch); } if (feof(fp)) { printf("\nEnd of file reached.\n"); } fclose(fp); return 0; }

clearerr() — Clear Error Flags

clearerr clears the error and EOF flags for a stream.

c

#include <stdio.h> int main(void) { FILE *fp = fopen("test.txt", "w+"); if (fp == NULL) { perror("Error opening file"); return 1; } fprintf(fp, "Hello"); /* Read to trigger EOF */ while (fgetc(fp) != EOF); if (feof(fp)) { printf("EOF encountered.\n"); } /* Clear EOF flag */ clearerr(fp); if (!feof(fp)) { printf("EOF flag cleared.\n"); } fclose(fp); return 0; }

Expected output:

text

EOF encountered. EOF flag cleared.

Exit Statuses

Use EXIT_SUCCESS and EXIT_FAILURE from <stdlib.h>.

c

#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> int main(void) { FILE *fp = fopen("nonexistent.txt", "rb"); if (fp == NULL) { fprintf(stderr, "Error: %s\n", strerror(errno)); exit(EXIT_FAILURE); } fclose(fp); exit(EXIT_SUCCESS); }

Manual Error Handling

Validate conditions before executing operations.

c

#include <stdio.h> int main(void) { int num1 = 10, num2 = 0; if (num2 == 0) { fprintf(stderr, "Error: Division by zero is not allowed\n"); return 1; } printf("Result: %d\n", num1 / num2); return 0; }

Expected output:

text

Error: Division by zero is not allowed

Full Practical Code Example with Explanation

This program demonstrates comprehensive error handling in a realistic scenario: a file processing utility with multiple error checks.

Program: Robust File Processor

c

/** * error_handling_demo.c * Demonstrates comprehensive error handling in C. * Includes errno, perror, strerror, ferror, feof, clearerr, and exit statuses. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #define MAX_LINE_LEN 256 #define MAX_FILENAME_LEN 100 /* Function prototypes */ int copyFile(const char *source, const char *destination); int readFileWithValidation(const char *filename); int divideNumbers(int a, int b, double *result); void demonstrateErrno(void); void demonstrateFileErrors(void); void demonstrateManualValidation(void); int main(void) { int choice; char buffer[MAX_LINE_LEN]; printf("=== Error Handling Demo ===\n"); while (1) { printf("\n--- Menu ---\n"); printf("1. Demonstrate errno, perror, strerror\n"); printf("2. Demonstrate File Errors (ferror, feof, clearerr)\n"); printf("3. Demonstrate Manual Validation\n"); printf("4. Copy File with Error Handling\n"); printf("5. Exit\n"); printf("Enter choice: "); fflush(stdout); if (fgets(buffer, sizeof(buffer), stdin) == NULL) { fprintf(stderr, "Error reading choice.\n"); return EXIT_FAILURE; } choice = atoi(buffer); switch (choice) { case 1: demonstrateErrno(); break; case 2: demonstrateFileErrors(); break; case 3: demonstrateManualValidation(); break; case 4: { char source[MAX_FILENAME_LEN]; char destination[MAX_FILENAME_LEN]; printf("Enter source file: "); fflush(stdout); if (fgets(source, sizeof(source), stdin) == NULL) break; source[strcspn(source, "\n")] = '\0'; printf("Enter destination file: "); fflush(stdout); if (fgets(destination, sizeof(destination), stdin) == NULL) break; destination[strcspn(destination, "\n")] = '\0'; if (copyFile(source, destination) == 0) { printf("File copied successfully.\n"); } break; } case 5: printf("Exiting.\n"); return EXIT_SUCCESS; default: printf("Invalid choice. Please try again.\n"); break; } } return EXIT_SUCCESS; } /* Demonstrates errno, perror, and strerror */ void demonstrateErrno(void) { printf("\n=== errno, perror, strerror ===\n"); /* Attempt to open a non-existent file */ FILE *fp = fopen("nonexistent_file.txt", "r"); if (fp == NULL) { printf("errno value: %d\n", errno); printf("strerror: %s\n", strerror(errno)); perror("perror message"); } /* Attempt to allocate too much memory */ printf("\nAttempting large allocation:\n"); int *ptr = malloc((size_t)-1); if (ptr == NULL) { printf("errno value: %d\n", errno); printf("strerror: %s\n", strerror(errno)); perror("malloc error"); } else { free(ptr); } } /* Demonstrates file error functions */ void demonstrateFileErrors(void) { printf("\n=== File Errors (ferror, feof, clearerr) ===\n"); /* Create a test file */ FILE *fp = fopen("test_errors.txt", "w"); if (fp == NULL) { perror("Error creating file"); return; } fprintf(fp, "Hello, Error Handling!\n"); fclose(fp); /* Read file with error checking */ fp = fopen("test_errors.txt", "r"); if (fp == NULL) { perror("Error opening file"); return; } int ch; while ((ch = fgetc(fp)) != EOF) { putchar(ch); } if (feof(fp)) { printf("End of file reached.\n"); } if (ferror(fp)) { printf("Read error occurred.\n"); } /* Clear EOF flag */ clearerr(fp); if (!feof(fp)) { printf("EOF flag cleared.\n"); } fclose(fp); /* Clean up */ remove("test_errors.txt"); } /* Demonstrates manual validation */ void demonstrateManualValidation(void) { printf("\n=== Manual Validation ===\n"); int a = 10, b = 0; double result; if (divideNumbers(a, b, &result) == 0) { printf("%d / %d = %.2f\n", a, b, result); } /* Array bounds checking */ int arr[5] = {1, 2, 3, 4, 5}; int index = 10; if (index >= 0 && index < 5) { printf("arr[%d] = %d\n", index, arr[index]); } else { fprintf(stderr, "Error: Index %d out of bounds.\n", index); } /* Input validation */ int age = -5; if (age < 0 || age > 150) { fprintf(stderr, "Error: Invalid age %d.\n", age); } } /* Safely divides two numbers */ int divideNumbers(int a, int b, double *result) { if (b == 0) { fprintf(stderr, "Error: Division by zero.\n"); return -1; } *result = (double)a / b; return 0; } /* Copies a file with comprehensive error handling */ int copyFile(const char *source, const char *destination) { FILE *src = fopen(source, "rb"); if (src == NULL) { fprintf(stderr, "Error opening source '%s': %s\n", source, strerror(errno)); return -1; } FILE *dst = fopen(destination, "wb"); if (dst == NULL) { fprintf(stderr, "Error opening destination '%s': %s\n", destination, strerror(errno)); fclose(src); return -1; } char buffer[4096]; size_t bytesRead; while ((bytesRead = fread(buffer, 1, sizeof(buffer), src)) > 0) { size_t bytesWritten = fwrite(buffer, 1, bytesRead, dst); if (bytesWritten != bytesRead) { fprintf(stderr, "Error writing to destination: %s\n", strerror(errno)); fclose(src); fclose(dst); return -1; } } if (ferror(src)) { fprintf(stderr, "Error reading from source: %s\n", strerror(errno)); fclose(src); fclose(dst); return -1; } fclose(src); fclose(dst); return 0; }

Explanation of Error Handling Usage

  1. errno: Stores error codes from failed functions.
  2. perror: Prints error messages to stderr.
  3. strerror: Returns error description string.
  4. ferror: Checks file operation errors.
  5. feof: Checks end-of-file condition.
  6. clearerr: Clears error and EOF flags.
  7. Manual validation: Checks conditions before operations.
  8. Exit statuses: Uses EXIT_SUCCESS and EXIT_FAILURE.

Expected Output (sample run)

text

=== Error Handling Demo === --- Menu --- 1. Demonstrate errno, perror, strerror 2. Demonstrate File Errors (ferror, feof, clearerr) 3. Demonstrate Manual Validation 4. Copy File with Error Handling 5. Exit Enter choice: 1 === errno, perror, strerror === errno value: 2 strerror: No such file or directory perror message: No such file or directory Attempting large allocation: errno value: 12 strerror: Cannot allocate memory malloc error: Cannot allocate memory --- Menu --- ... Enter choice: 3 === Manual Validation === Error: Division by zero. Error: Index 10 out of bounds. Error: Invalid age -5. --- Menu --- ... Enter choice: 5 Exiting.

Common Pitfalls, Edge Cases & Memory Considerations

1. Not Checking Return Values

Ignoring return values can lead to undefined behavior.

c

FILE *fp = fopen("file.txt", "r"); fgetc(fp); // BUG: fp may be NULL

Solution: Always check return values before using results.

2. Using errno Without Checking Function Return

errno may contain stale values from previous errors.

c

remove("file.txt"); printf("%s\n", strerror(errno)); // BUG: errno may be stale

Solution: Only check errno after a function returns an error indicator.

3. Not Resetting errno

Before calling a function that sets errno, reset it to 0.

c

errno = 0; long val = strtol("123", NULL, 10); if (errno == ERANGE) { ... }

Solution: Set errno = 0 before functions where error detection via errno is needed.

4. Forgetting to Close Files on Error

Early returns without closing files leak resources.

c

FILE *fp = fopen("file.txt", "r"); if (someError) { return -1; // BUG: fp not closed }

Solution: Close files before returning on error paths.

5. Using feof as Loop Condition

Using feof as loop condition is an anti-pattern.

c

while (!feof(fp)) { // BUG: may process last character twice fgets(buffer, sizeof(buffer), fp); }

Solution: Use the return value of the read function.

6. Not Checking malloc Return

Using malloc result without checking for NULL.

c

int *ptr = malloc(1000000000); ptr[0] = 10; // BUG: ptr may be NULL

Solution: Always check if (ptr != NULL) after allocation.

7. Division by Zero

Division by zero causes undefined behavior (crash on most systems).

c

int result = a / b; // BUG if b == 0

Solution: Check if (b != 0) before division.

8. Array Bounds Access

Accessing arrays out of bounds is undefined behavior.

c

int arr[5]; arr[10] = 42; // BUG: out of bounds

Solution: Validate indices before access.

9. Memory Leaks on Error Paths

Forgetting to free allocated memory on error paths.

c

int *ptr = malloc(100); if (someError) { return -1; // BUG: memory leak } free(ptr);

Solution: Free memory before returning on error paths.

10. Silent Failures

Ignoring errors entirely leads to hard-to-debug issues.

Solution: Always report errors with descriptive messages.


Summary & Best Practices

Error handling in C relies on return values, errno, and manual validation. Proper error handling ensures robust programs that gracefully handle failures.

Key Takeaways

  • Check return values of functions (NULL, -1, EOF).
  • Use errno for error codes.
  • Use perror and strerror for descriptive messages.
  • Use ferror for file operation errors.
  • Use feof for end-of-file detection.
  • Use clearerr to reset error flags.
  • Use EXIT_SUCCESS and EXIT_FAILURE for program exit.
  • Validate conditions manually (division by zero, bounds, input).

Best Practices

  1. Always check function return values.
  2. Use perror or strerror(errno) for error reporting.
  3. Reset errno to 0 before functions that set it.
  4. Close files and free memory on error paths.
  5. Use early returns for error handling to keep code clean.
  6. Validate user input before processing.
  7. Check for division by zero.
  8. Validate array indices before access.
  9. Use EXIT_SUCCESS and EXIT_FAILURE for program termination.
  10. Test error paths to ensure proper handling.

By mastering error handling in C, you’ll write robust, reliable programs that handle failures gracefully and prevent crashes.