node icon indicating copy to clipboard operation
node copied to clipboard

It's not clear if FileHandle close() and stat() can fail

Open lifeisfoo opened this issue 1 year ago • 3 comments

Affected URL(s)

https://nodejs.org/api/fs.html

Description of the problem

Using Node.js FileHandles you can open a file, get a fs.Stats object from the handle and then close the file.

// first part of the function copied from the close() doc
// https://nodejs.org/api/fs.html#filehandleclose

import { open } from 'node:fs/promises';

function processFile(path)
    let filehandle;
    try {
        filehandle = await open(path, 'r');
    } finally {
        await filehandle?.close();
        return;
    }
 
    const fileStat = await filehandle.stat(); //  1
    // do something ...
    await filehandle.close();  // 2
}

Node.js documentation says:

filehandle.stat([options])
    Returns: <Promise> Fulfills with an <fs.Stats> for the file.

filehandle.close()
   Returns: <Promise> Fulfills with undefined upon success.

From these statements its' not clear if both methods can fail, or not, during the execution and in which cases. Do we need to wrap FH.stat() and FH.close() calls in try-catch blocks? Or, since the FileHandle has been already opened, they can't fail and it is safe to use them without a try-catch block?

I've also tried replacing the "do something" line with an await setTimeout(10000) in order to delete the opened file during the execution. But, at least on Linux, the deletion (rm command) is delayed until all file descriptors stop using it. So, even in that case both methods don't fail. Same story using chmod and chown... Is there any platform specific behaviours?

lifeisfoo avatar Dec 12 '23 16:12 lifeisfoo

@lifeisfoo handle.stat() and handle.close() can definitely fail for example on EBADF. Quoting documentation: “Bad file descriptor.” For example, I/O on a descriptor that has been closed or reading from a descriptor open only for writing (or vice versa). Example:

import { open } from 'node:fs/promises'
const handle = await open('./package.json');
await handle.close();
await open('./package.json');
await handle.stat(); // will reject EBADF

cc @nodejs/fs

marco-ippolito avatar Dec 13 '23 04:12 marco-ippolito

Yes, in your code FileHandle.stat() can throw an error BUT only because you have inserted "a bug" (closing the handle before calling stat()).

I'll try to rephrase my question splitting it.

FileHandle.stat() rejection

If I have a valid FileHandle pointing to an existing open file with read permissions, is there a case when a call to FileHandle.stat() can fail (excluding a wrong usage like the one mentioned above)?

Sample code:

import { open } from "fs/promises";

async function processFile(path) {
    let filehandle;
    try {
        filehandle = await open(path, "r");
    } catch (e) {
        await filehandle?.close();
        return;
    }

    const fileStat = await filehandle.stat(); // <<<---- could it fail?
    //...
}

Since this call is based on uv_fs_stat I'm expecting the same errors from the underlyning fstat and, as you can see, they are not relevant if you already have a valid file description.

  EACCES Search permission is denied for one of the directories in the path prefix of pathname.  (See also path_resolution(7).)
  EBADF  fd is not a valid open file descriptor.
  EBADF  (fstatat()) pathname is relative but dirfd is neither AT_FDCWD nor a valid file descriptor.
  EFAULT Bad address.
  EINVAL (fstatat()) Invalid flag specified in flags.
  ELOOP  Too many symbolic links encountered while traversing the path.
  ENAMETOOLONG   pathname is too long.
  ENOENT A component of pathname does not exist or is a dangling symbolic link.
  ENOENT pathname is an empty string and AT_EMPTY_PATH was not specified in flags.
  ENOMEM Out of memory (i.e., kernel memory).
  ENOTDIR  A component of the path prefix of pathname is not a directory.
  ENOTDIR  (fstatat()) pathname is relative and dirfd is a file descriptor referring to a file other than a directory.
  EOVERFLOW pathname or fd refers to a file whose size, inode number, or number of blocks cannot be represented in, respectively, the types off_t, ino_t, or blkcnt_t.  This error can occur when, for example, an application compiled on a 32-bit platform without -D_FILE_OFFSET_BITS=64 calls stat() on a file whose size exceeds (1<<31)-1 bytes.

Are they possible error thrown by FileHandle.stat()? Are there others UV related errors that can be thrown by the function? Are they fatal or can they be handled by our code?

FileHandle.close() rejection

If I have a valid FileHandle pointing to an existing open file, is there a case when a call to FileHandle.close() can fail?

Sample code:

import { open } from "fs/promises";

async function openAndCloseFile(path) {
    let filehandle;
    try {
        filehandle = await open(path, "r");
    } catch (e) {
        await filehandle?.close();
        return;
    }

    await filehandle.close(); // <<<---- could it fail?
}

Like stat, even this call is based on uv_fs_close that uses the POSIX close. Here the list of errors is shorter than before but exists. Are those errors returned by Node.js when using the FileHandle.close()? Or, are they wrapped or muted? Are FileHandle.close() related errors tested somewhere?

Docs, promises and rejections

The vast majority of the FileHandle related methods say:

Returns: <Promise> Fulfills with [something] upon success.

So I'me expecting that they can't fail and so I can avoid try-catching them.

But you have pointed out a case of failure (EBADF) for the FileHandle.stat() function that could be returned from any of those functions. And it does not appear in the fs docs.

Information that should be included in the fs documentation

This is what I'm expecting to see in any of the fs module functions' doc:


filehandle.close()

Returns: Promise Fulfills with undefined upon success or it is rejected with Error if it fails. See close(2) for a list of possible error codes and fs errors explained for more.

Closes the file handle after waiting for any pending operation on the handle to complete.


The fs errors explained section will explain that fs error codes are strings available in the error's code property.

{ code: "EBADF" }

Moreover, each fs function should list other errors that can be thrown for that function, including fatal ones.


In order to write safe, robust and concise Node.js code it's essential to find such information in each function docs. Otherwise we will end up adding endless try-catch blocks around each call even if we don't know why.

lifeisfoo avatar Dec 13 '23 16:12 lifeisfoo

I think since FS implementation greatly vary by OS, you should assume all callbacks can fail.

ShogunPanda avatar Feb 05 '24 15:02 ShogunPanda