If closing a file fails then you treat it the same as how you would treat a write failure:
int err = 1;
FILE *f = fopen("whatever.txt", "w");
if(f){
if(5 == fwrite("Hello", 1, 5, f))
err = 0;
if(0 != fclose(f))
err = 1;
}
return err;
Code which writes to files and doesn't check for errors on close is subtly incorrect, although my understanding is that kernel devs bend over backwards to make failure unlikely, probably because everybody does it incorrectly anyway.- is fp NULL or already already closed? return error
- call fflush() and return error if it fails (fflush also happens in userland, it does a seek() then a write() of the userland buffer)
- call close() and return error if it fails
close() follows essentially the same process inside the kernel: check fd is valid, call flush() (this time truly to disk), close it.
The primary way to deal with error-on-clean-up in RAII languages is to not rely exclusively on RAII for it. Rust's File type, for example, has sync_data and sync_all methods (which, to be fair, only even need to be called for writable file handles). I don't think there's anything wrong with this approach, but it ends up being just as explicit and therefore forgettable as defer.
It should be noted that you can (at least in Rust) actually implement defer using RAII; see e.g. the scopeguard crate. Since RAII is block-scoped, this defer is also block-scoped (like Zig) rather than function-scoped (like Go).
A system that is based on linear types would have an advantage here. In a linear type system, the compiler guarantees that you always call the cleanup function (destructor) exactly once. With such a system, you can have the destructor return an error result, and since the call will always be explicitly written in the code (rather than generated automatically by the compiler), there will always be an explicit errorn handling code branch.