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
- Serialize usage: await every operation before calling close(), ideally open → work → close within one function scope.
- Before reusing a possibly-closed handle, guard with fh.fd !== -1 (the polyfill reports -1 after close).
- Restructure to per-task handles: open the file again instead of sharing one long-lived FileHandle.
- 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
- Own the lifecycle: open, use, and close the handle in one function scope with try/finally
- Await every operation before close() — no fire-and-forget writes on a shared handle
- Guard reuse with fh.fd !== -1
- Audit error and abort paths for early close() calls
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
- ERR_METHOD_NOT_IMPLEMENTED
- A file exists at the destination: ${destStr}
- ERR_MISSING_ARGS
- ERR_DIR_CLOSED
- No callback function supplied
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/68925d4138032b68.
Report an issue: GitHub.