This is really more about POSIX and FS semantics than C (although ultimately you end up using the C ABI or kernel system calls, which are closer to C than e.g. Python).
POSIX gives implementations enough leeway to have close() and fsync() do pretty much whatever they want as far as who returns what error goes, as long as not returning an error means your data made it to storage.
But in practice close() is typically 1=1 mapped to the file itself, while fsync() is many=1 (even though both take a "fd"). I.e. many implementations (including the common consumer OS's like Windows, OSX & Linux) have some notion of unrelated outstanding I/O calls being "flushed" by the first process to call fsync().
IIRC on ext3 fsync() was pretty much equivalent to sync(), i.e. it would sync all outstanding I/O writes. I believe that at least Windows and OSX have a notion of doing something similar, but for all outstanding writes to a "leaf" on the filesystem, i.e. an fsync() to a file in a directory will sync all outstanding I/O in that directory implicitly.
Of course none of that is anything you can rely on under POSIX, where you not only have to fsync() each and every file you write, but must not forget to also flush the relevant directory metadata too.
All of which is to say that you might be out of space when close() happens, but by the time you'd fsync() you may no longer be out of space, consider a write filling up the disk and something that frees up data on disk happening concurrently.
If you know your OS and FS semantics you can often get huge speedups by leaning into more lazily syncing data to disk, which depending on your program may be safe, e.g. you write 100 files, fsync() the last one, and know the OS/FS syncs the other 99 implicitly.
But none of that is portable, and you might start losing data on another OS or FS. The only thing that's portable is exhaustively checking errors after every system call, and acting appropriately.