> The only way to properly close a file is to use regular old explicit control flow with if statements. Then you can handle the errors in the normal way.

Other than crashing the program, what could you possibly do?

This is what portable software has to work with [1]:

If close() is interrupted by a signal that is to be caught, it shall return -1 with errno set to [EINTR] and the state of fildes is unspecified. If an I/O error occurred while reading from or writing to the file system during close(), it may return -1 with errno set to [EIO]; if this error is returned, the state of fildes is unspecified.

And it's not just the standard being dumb. Here's what Linux's manual page says about it [2]:

Retrying the close() after a failure return is the wrong thing to
do, since this may cause a reused file descriptor from another
thread to be closed. This can occur because the Linux kernel
always releases the file descriptor early in the close operation,
freeing it for reuse; the steps that may return an error, such as
flushing data to the filesystem or device, occur only later in
the close operation.

Many other implementations similarly always close the file
descriptor (except in the case of EBADF, meaning that the file
descriptor was invalid) even if they subsequently report an error
on return from close(). POSIX.1 is currently silent on this
point, but there are plans to mandate this behavior in the next
major release of the standard.

A careful programmer who wants to know about I/O errors may
precede close() with a call to fsync(2).

The EINTR error is a somewhat special case. Regarding the EINTR
error, POSIX.1-2008 says:

If close() is interrupted by a signal that is to be
caught, it shall return -1 with errno set to EINTR and the
state of fildes is unspecified.

This permits the behavior that occurs on Linux and many other
implementations, where, as with other errors that may be reported
by close(), the file descriptor is guaranteed to be closed.
However, it also permits another possibility: that the
implementation returns an EINTR error and keeps the file
descriptor open. (According to its documentation, HP-UX's
close() does this.) The caller must then once more use close()
to close the file descriptor, to avoid file descriptor leaks.
This divergence in implementation behaviors provides a difficult
hurdle for portable applications, since on many implementations,
close() must not be called again after an EINTR error, and on at
least one, close() must be called again.

And, just to finish it off, Windows is completely silent on this matter. It mentions EBADF, and that's it [3]. The CloseHandle() function, which POSIX close() (a.k.a. _close()) uses under the hood, also doesn't seem to say anything about whether CloseHandle() failing on a file leaves the handle opened, closed, or in limbo.

And since all of these documentations reserve the right to not actually flush the file when it's closed, any warnings or errors are going to be "best-effort" anyway. If you really want to make sure the file actually gets written, call fsync().

[1]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/c...

[2]: https://www.man7.org/linux/man-pages/man2/close.2.html

[3]: https://docs.microsoft.com/en-us/cpp/c-runtime-library/refer...