I still think that most of this is irrelevant. Especially for fread(), all I care about when I'm doing error handling is "if I didn't get all the data I requested for, why didn't I?" - if it's ferror() I may want to retry reading from the same place where my fread() began, but if it's feof() I probably don't. In this case, I can only meaningfully handle the error if I know exactly which of my fread()s failed for which reason.

Sure, the actual IO that caused that error may have happened 3 hours ago on a different thread, and 100 fread()s may have been able to read data from the buffer since then without caring about the IO error. When the 101st one actually sees it can't read enough, now I have to care, and I want to find out as easily as possible, and atomically. I don't want my (buffered) read to fail and then be unable to find out why because some other thread reset my context.

Now, for fwrite() the issue is indeed quite different, since my code doesn't depend on the result of the operation in the same way (I am ultimately invoking it for its side effect, not for its result, unlike fread() ). And you're right, by its very nature, fwrite() can't meaningfully report all errors I would care about (and even fflush() can't, as the creators of Postgres found out the hard way). But even here, the problem is not related in any way to how you chose to report exceptions, and ferror() is not in any way reliable for ensuring transactional access, any more than exceptions would be.

In fact, the newer async IO operations in both Windows and Linux (I/O Rings and io_uring) also return the result in place, not on some context object. Looking more closely at the Linux API, each CQE (completion queue event) has a `res` field that represents the error code from the results of 1 specific SQE (submission queue entry); this entirely replaces the state-based global errno variable that was sometimes used for other system calls. So, the pattern here is: start an async IO task, check the error returned in case it wasn't able to even start, wait for it to complete, check the error this specific task produced when it completed.

The same could be done with a hypothetical safe write operation: initiate a call to definitely_write(), and get back some kind of handle that allowed us to wait until those particular bytes were physically written to disk (or at the very least to the disk driver, since those now often do their own level of buffering...). The wait() operation on this handle would then return an error code if the write failed for any reason. Of course, in the case of an actual IO error, it's very likely that many different such handles will return the same error when someone waits for them to finish, since the error itself was not specific to some specific byte. And also of course, it is possible that the bytes from one particular definitely_write() that returns an error on wait() have actually been written to disk - the error just means that the IO layer can't guarantee this to us.

But even so, there's no reason and no benefit to decoupling the error reporting from the operations themselves, especially if the error state is transient - and all modern IO APIs, low or high level, recognize this.