denoland/deno · error · Error

EBADF

EBADF

Error message

file closed

What it means

Every FileHandle method in Deno's node:fs/promises polyfill routes through fsCall, which first calls assertNotClosed (handle.ts:614-622). After close(), the handle's backing rid becomes -1, and any subsequent read/write/stat/truncate/appendFiles call throws Error('file closed') with code EBADF and syscall set to the method name. It mirrors Node's EBADF for operating on an already-closed descriptor; note handle.fd also reports -1 (handle.ts:160-162, 395).

Source

Thrown at ext/node/polyfills/internal/fs/handle.ts:618

      lazyFs().write(
        rid,
        str,
        position,
        encoding,
        (err: Error, bytesWritten: number, buffer: Buffer) => {
          if (err) reject(err);
          else resolve({ buffer, bytesWritten });
        },
      );
    });
  }
}

function assertNotClosed(rid: number, syscall: string) {
  if (rid === -1) {
    const err = new Error("file closed");
    throw ObjectAssign(err, {
      code: "EBADF",
      syscall,
    });
  }
}

type FileHandleFn<P, R> = (...args: [number, ...P[]]) => Promise<R>;

async function fsCall<P, R, T extends FileHandleFn<P, R>>(
  fn: T,
  fnName: string,
  handle: FileHandle,
  ...args: P[]
): Promise<R> {
  assert(
    handle[kRefs] !== undefined,
    "handle must be an instance of FileHandle",
  );
  assertNotClosed(handle.fd, fnName);

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Serialize usage: await every operation before calling close(), ideally open → work → close within one function scope.
  2. Before reusing a possibly-closed handle, guard with fh.fd !== -1 (the polyfill reports -1 after close).
  3. Restructure to per-task handles: open the file again instead of sharing one long-lived FileHandle.
  4. Search for close() calls in error/abort paths — an early close is the usual culprit.

Example fix

// before
const fh = await fs.promises.open('log.txt', 'a');
await fh.close();
await fh.write('line\n'); // EBADF: file closed, syscall 'write'

// after
const fh = await fs.promises.open('log.txt', 'a');
try {
  await fh.write('line\n');
} finally {
  await fh.close();
}
Defensive patterns

Strategy: type-guard

Type guard

/** True while the FileHandle is still usable (Deno polyfill sets fd to -1 after close). */
function isFileOpen(fh) {
  return typeof fh.fd === 'number' && fh.fd !== -1;
}

Try / catch

try {
  await fh.write(data);
} catch (err) {
  if (err.code === 'EBADF' && err.message === 'file closed') {
    fh = await fs.promises.open(path, flags); // reopen and retry once
    await fh.write(data);
  } else throw err;
}

Prevention

When it happens

Trigger: await fh.close() followed by fh.read(...); fire-and-forget writes racing an explicit close; retry wrappers that close on the first error and then reuse the handle; a second read after readFile helpers already closed the handle (promises.ts handleFdClose).

Common situations: Cleanup in finally{} running while queued writes are still in flight; queue consumers that close after the first item and keep processing; passing FileHandle across module boundaries with no single owner; double-close followed by use.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/68925d4138032b68. Report an issue: GitHub.