C is not your friend: the errno interface
Some of the library functions in the C standard (and a lot more in POSIX) set errno to report what went wrong. The interface is pretty simple: the return value of the function tells you that there's been an error, and then errno tells you what it was.
And since it's a standard interface and people are used to it, you may be tempted to use it in your own library APIs. After all, it's got a few advantages. You can use it as a sort of poor man's exception scheme by bailing out on errors and letting the caller read errno to see what caused your code to fail. And you get helper functions like strerror() and warn() for free. But resist this temptation! In the end it will be more trouble than it's worth:
First of all, even with 70–150 error codes to choose from, you'll probably have some errors that don't fit nicely into the scheme, and since it's system-defined you can't extend it. That leads to weird decoder-ring interfaces where you reuse, say, EIO, because it's the closest thing to what you meant. And then your caller can't tell whether an errno of EIO means this new thing, or just the result of an internal write() call that failed.
Second, propagating errno properly is actually hard. In code like this, for example:
fd = open(path, O_RDONLY); if (fd < 0) { fprintf(log_fd, "could not open %s\n", path); return -1; }
an error in the call to fprintf() could set errno again, replacing the error you were trying to report. So you end up with a lot of ugly boilerplate scattered around that saves errno, calls a function, and puts errno back the way it was.
Third, errno values aren't portable. On my linux machine, errno 38 is ENOSYS; on a FreeBSD box it's ENOTSOCK; on Solaris, EL2NSYNC. So you can't send them over the network, or save them to a file that might be read on another machine, or log them without converting them to text first.
So instead, consider defining your own error codes, with numeric values that are part of the interface, and with whatever specific meanings you need. And, if you can, return them as the actual return value of the function rather than needing a side-channel like errno:
Return a signed integer where all negative values are error codes.
Return a pointer-width unsigned integer where certain special values are encoded errors. The linux kernel does this internally, where all addresses below 1 page are error codes; you could use the address of a static datastructure, or an architecturally invalid address.
Return a struct with an error code and other values packed together.
If all else fails, return an error code directly and write any other outputs to a caller-supplied pointer.
And when dealing with library functions, get the error out of errno as quickly as possible, before something else clobbers it!