Without exceptions, throw, or try/catch, C functions signal failure through their return value — commonly NULL for a function returning a pointer, -1 for a function returning an otherwise-nonnegative int, or an explicit status/error code. Many standard library functions additionally set the global errno variable to a specific error code on failure, which strerror() can translate into a human-readable message — though the ISO C standard itself only guarantees this for a handful of functions (the math.h family, setting EDOM/ERANGE); for many others, including fopen() failing with a specific code like ENOENT, it's POSIX that requires errno to be set meaningfully, not ISO C itself (confirmed on cppreference's own fopen reference: "POSIX requires that errno be set"). On Linux, macOS, and other POSIX systems this distinction rarely bites in practice, but it means the errno-after-fopen-failure pattern isn't guaranteed to be portable to a non-POSIX C implementation. Critically, nothing forces a caller to check any of this — an ignored error return simply lets execution continue with invalid data or a null pointer, silently, until something else breaks later.
Checking return values: entirely manual, entirely optional
Nothing forces this check to happen — skip it, and a failed fopen() silently returns NULL, which then crashes the program the instant something tries to read from it, often several lines away from the actual mistake.
FILE *f = fopen("data.txt", "r");
if (f == NULL) { // MUST check manually — nothing does this for you
perror("fopen failed"); // prints a message using errno's current value
return 1;
}
// safe to use f from hereerrno and strerror: more detail on WHY it failed
errno alone is just a numeric code; strerror() translates it into a readable message — always check errno immediately after the failing call, since a subsequent successful function call can overwrite it. Both halves of this example lean on POSIX, not just ISO C: the specific code ENOENT ("no such file") is a POSIX-defined errno value rather than one the C standard itself specifies, and the exact wording "No such file or directory" is implementation-defined text, conventional on POSIX systems (Linux, macOS) but not something the standard promises verbatim.
#include <errno.h>
#include <string.h>
FILE *f = fopen("missing.txt", "r");
if (f == NULL) {
printf("Error: %s\n", strerror(errno)); // e.g. "No such file or directory"
}A common pattern: an explicit status/result type
Returning a dedicated enum status alongside output through a pointer parameter is a common idiom for functions that genuinely can't use a sentinel value (like -1 or NULL) to mean "failure," because every possible return value is a legitimate success case.
typedef enum { OK, ERR_DIVIDE_BY_ZERO } Status;
Status safeDivide(int a, int b, int *result) {
if (b == 0) {
return ERR_DIVIDE_BY_ZERO;
}
*result = a / b;
return OK;
}
int result;
if (safeDivide(10, 0, &result) != OK) {
printf("division failed\n");
}