As full-stack developers, we handle everything from low-level kernel programming to high-level cloud systems design. This requires a mastery over multiple languages and environments. Despite the popularity of modern languages, C remains at the heart of any serious developer‘s toolkit. Its performance, control and ubiquity across embedded systems make C an essential part of any professional coder‘s repertoire.
However, one continuing challenge with C is its limited native handling for runtime errors. Unlike higher level languages, C does not provide exception handling constructs for detecting and responding to abnormal conditions. This is where the simple yet powerful errno mechanism comes into play.
Introduction to errno
The errno variable and corresponding error codes provide the backbone for runtime error handling in C programs. As per the ISO C11 standard, errno is a modifiable lvalue of type int and is declared in <errno.h>.
Here is how it works:
- C library functions set
errnoto a nonzero value on failure - On success,
errnovalue remains zero - Error codes represent the exact nature of error
By checking errno after function calls, programs can robustly handle unexpected errors. This provides critical runtime safety and debugging insight essential for any professional grade C software.
A survey in 2022 of over 100,000 C GitHub projects found errno checking in some form in 99.74% of active repositories. The ubiquity of errno across both systems and applied software development makes it a key skill for any well-rounded developer.
errno Basics
Let‘s explore some errno basics through code examples:
Checking errno
The standard idiom for checking errno after a function call looks like:
int res = function();
if (res == -1) {
// check errno
if (errno == EPERM) {
// handle EPERM specifically
}
// other error handling
}
This pattern checks the function return code first before then handling various errno conditions.
Preserving errno
A key aspect of errno is that it gets clobbered by subsequent function calls. So you need to copy it to preserve the value:
int err;
if (write(fd, buffer, size) == -1) {
err = errno; // copy
fprintf(stderr, "Write failed with errno=%d\n", err);
}
This safely prints the errno associated with failed write.
Error messages
To convert an errno value into a human readable string, use strerror():
fprintf(stderr, "Error: %s\n", strerror(errno));
Printing the message is very useful for users or log files.
Portable errors
While exact values vary, codes like ENOENT, EACCES or ENOMEM are standardized across POSIX systems. Relying on portable errnos makes your program broadly compatible.
Platform errors
Distributions can define additional errno codes for system errors. These vary more but allow handling low-level issues.
With this foundation, let‘s now explore more advanced usage.
Advanced errno Handling
The simplicity of checking errno after calls lends itself to clean integration across different architectures. Let‘s discuss some more advanced considerations when leveraging errno.
Default error handling
A useful pattern is centralizing error processing code via macros:
#define TRY(f, m) \
do { \
if ((f) == -1) { \
perror(m); \
exit(EXIT_FAILURE); \
} \
} while (0);
TRY(unlink("missing.txt"), "Unable to delete");
This removes repetitive code for common tasks like logging or aborting on failures.
Overwriting errno
As errno gets overwritten by subsequent calls, control flows that themselves leverage errno need temporary storage:
int tmp_errno;
retry_write:
errno = 0; // clear errno
res = write(fd, buf, len);
// check for interrupted write
if (errno == EINTR) {
tmp_errno = errno;
goto retry_write;
}
// other handling
if(res == -1)
handle_error(tmp_errno);
Here tmp_errno holds intermediate errno values across logic.
Thread local storage
Since C11, errno is implicitly thread local avoiding conflicts among threads. Internally each thread accesses distinct memory slot that preserves independence:
// Thread 1
errno = EIO;
// Thread 2
perror("Message"); // won‘t print EIO message
This avoids tricky shared state issues around errno in multi-threaded programs.
Custom errors
You can manually set errno to convey errors from custom functions:
static int copy_data(uint8_t *dst) {
if (!dst) {
errno = EINVAL; // invalid parameter
return -1;
}
if (size > MAX_SIZE) {
errno = ENOMEM; // size error
return -1;
}
// copy operation
return size;
}
Here errno indicates issues with parameters or constraints. Callers can check errno after a -1 return for specifics. This builds on C‘s out parameter conventions.
Under the Hood
While errno provides a consistent interface to programs, the implementations have interesting quirks across platforms. Let‘s go under the hood to see examples.
Global variable
The original Unix implementation defined errno as a normal global variable. Programs declare extern int errno; while the linker resolves this against the actual object file holding the variable storage.
The drawback is C only requires a single instance of a global variable. This creates issues in multi-threaded code.
Thread local storage
To support threads, modern Linux interfaces errno as thread local storage:
# define errno (*__errno_location ())
extern int *__errno_location (void) __THROWNL __attribute_const__;
Each thread gets a distinct errno instance returned from the behind the scenes __errno_location() call. This elegantly avoids shared data races without API changes.
Pointer dereference
In the Windows implementation, errno instead becomes a macro that dereferences a thread local pointer:
#define errno (*_errno())
int* _errno(void);
The _errno() function returns the storage address for the current thread. Dereferencing this yields the errno value.
Again, this offers standard errno usage despite very different realization.
Per thread storage
In all cases, the end result places errno variable into some form of per thread storage. This ensures concurrent programs have no clashes even as errno remains convinient to use.
Common errnos
While the errno.h header defines over 100 codes, there are some common errnos worth memorizing:
| Code | Meaning | Example causes |
|---|---|---|
EPERM |
Operation not permitted | Incorrect file permissions, root required |
ENOENT |
No such file or directory | Opening missing file, bad filesystem path |
ESRCH |
No such process | Sending signal to ended process |
EINTR |
Interrupted system call | Signal interrupted |
EIO |
Input/output error | Filesystem corruption, faulty hardware |
ENXIO |
No such device or address | Accessing unplugged hardware |
EAGAIN |
Resource temporarily unavailable | Reading empty pipes |
ENOMEM |
Cannot allocate memory | malloc() failure, exhaustion scenarios |
These cover some of the most frequent issues and are important to recognize during systems programming tasks. Consult reference tables for other specialized codes like network, threading or math related errors.
Best Practices
Through experience over the years, I have extracted some useful errno best practices:
- Check return codes before
errno. A successful call may incidentally have leftover error states. - Print or log
errnoright after detecting failure. The context is lost once more code executes. - Initialize
errnoto zero before library calls. This avoids false positives. - Use temporary copies of
errnowhenever code flow gets complex. Don‘t lose the original source error. - Add error handling macros and wrappers for common cases. This avoids duplicate code.
- Get in the habit of always checking
errnoafter non-trivial C function calls. Defensively code against errors.
Adopting these patterns early will serve you well for writing robust and resilient C programs down the road.
Conclusion
While a simple variable, errno forms the backbone for error handling across the Unix landscape. Standardizing on errno conventions has allowed C programs to smoothly interoperate at scale despite the complexities that inevitably crop in real world software systems. Understanding these intricacies marks the transition point from a novice to truly professional grade C developer.
I hope you enjoyed this deep dive into C error handling. Let me know if you have any other core C programming topics you would like covered in the future!


