In most languages, including C, we usually produce the most maintainable code by dealing with acquire / free at the same level:
fd = open(...)
consume(fd);
close(fd);
This applies to locks, database connections, malloc'd memory, and many other resources. We strive to support "local analysis" by some poor maintenance engineer who wants to read and understand the invariants without needing to vertically scroll the code.
Real C code would need to examine fd and errno for errors,
and perhaps goto fail.
Other languages may let you use exceptions to ensure that
contracts
are satisfied and that a close() finally happens.
Take advantage of your language's support for cleanup.
For example golang offers defer,
while python offers with context handlers.
Sometimes resources carry global implications. For example, malloc'ing a kilobyte is no big deal, but allocating a gigabyte might be; it counts against the global RAM resource. And if the consumer needs to sequentially perform N "large" operations, each requiring more than 1/N-th of total RAM, then violating the usual layering constraint might be warranted. But this should be more the exception than the rule.