I deleted files in Node.js for a week — here’s what actually worked

I had one job: clean up junk. Logs, temp pics, old build folders. Simple, right? I thought so. Then Windows threw a fit. Linux shrugged. macOS just stared at me. You know what? I learned a lot.

Here’s how I deleted files in Node.js, what broke, and what I use now.

My setup (so you know where I’m coming from)

  • Node.js 20 on a MacBook Air (M2)
  • Windows 11 on a Lenovo laptop
  • A small photo tool and a CLI that watches logs
  • Lots of tiny files, and a few loud ones (big zips, cache folders)

I wrote scripts that ran after builds, and jobs that cleaned old uploads. I wanted quiet, fast, and safe.

When the file exists for sure, unlink feels nice and clean; this step-by-step guide on deleting a file from disk in Node.js mirrors my own starter approach.

import { promises as fs } from 'node:fs';

try {
  await fs.unlink('./tmp/test.txt');
  console.log('deleted');
} catch (err) {
  if (err.code === 'ENOENT') {
    console.log('file not found');
  } else {
    throw err;
  }
}

Good for single files. Clear error when the file is missing. But I got tired of checking. I just wanted, “try to delete and don’t scream.”

If you want a blow-by-blow account of exactly how a week of nothing but unlinks went, this narrative lines up eerily well with my own bruises.

My go-to now: fs.rm with force

fs.rm is my steady tool—and this breakdown of the fs.rm method shows why. It can remove files or folders. It also has a “don’t fuss” switch.

import { promises as fs } from 'node:fs';

// delete a file; no error if missing
await fs.rm('./output.txt', { force: true });

// delete a folder
await fs.rm('./build', { recursive: true, force: true });
  • force: true means “it’s okay if it’s gone already”
  • recursive: true is for folders

This cut my try/catch code a lot. It made my cleanup scripts feel calm.
For an extended walkthrough of robust file-handling patterns in Node, I highly recommend this concise article on ImprovingCode.

I spilled all the gritty details in my honest take on deleting files in Node.

Sync vs async: when I block on purpose

For small build steps, I sometimes go sync. It’s blunt, but it works, and I don’t have to juggle async.

import fs from 'node:fs';

fs.rmSync('./dist', { recursive: true, force: true });

I use this in a prebuild script. Short, blocking, done. For apps and servers though, I stay async.

Windows gotcha: file is “busy” (EPERM/EBUSY)

This one bit me. On Windows, if a file is still open, you can’t delete it. A log stream made me stare at EPERM at 1 a.m. The fix: close the file, then delete.

import fs from 'node:fs';
import { finished } from 'node:stream/promises';

const ws = fs.createWriteStream('log.txt');
ws.end('done'); // finish writing
await finished(ws); // wait for close on Windows
await fs.promises.rm('log.txt', { force: true });

If you’re writing, wait for the stream to finish. If a photo is open in another app, close the app first. Sounds silly, but that was the issue.

After an especially long night of slogging through EPERM errors, I found I sometimes just needed a mental reset. If endless console logs ever have you feeling the same, a quick detour to a live‐cam chat site like JerkMate can provide a fun, no-strings way to unwind before jumping back into your code — a surprising but effective break that leaves you refreshed for the next debugging round.

For developers who happen to be in Kentucky and prefer a real-world diversion, the local directory at AdultLook Louisville offers up-to-date listings and reviews so you can step away from the screen, enjoy some in-person downtime, and return to your debugging session with a clearer head.

During one frustrated night I even toyed with just uninstalling Node altogether; if that’s the rabbit hole you’re staring at, this piece captures the journey.

Linux/macOS note: permissions

On my Ubuntu box, I saw EACCES once. The file was root-owned from a Docker run. I changed the owner, then removed it.

sudo chown -R $USER:$USER ./cache
node clean.js

If you see EACCES, check owners and modes. Quick fix with chmod or chown helps.

Real scripts I used

Here are the ones I run the most.

  1. Clean logs after my watcher stops
    “`js
    import { promises as fs } from 'node:fs';

async function cleanLogs() {
await fs.rm('./logs/app.log', { force: true });
await fs.rm('./logs/error.log', { force: true });
}

await cleanLogs();


2) Remove build folders before a fresh roll
```js
import { promises as fs } from 'node:fs';

await fs.rm('./dist', { recursive: true, force: true });
await fs.rm('./.cache', { recursive: true, force: true });
  1. Delete old uploads (7 days or older)
    “`js
    import { promises as fs } from 'node:fs';
    import path from 'node:path';

const UPLOADS = './uploads';

async function cleanOldUploads(days = 7) {
const maxAge = days * 24 * 60 * 60 * 1000;
const files = await fs.readdir(UPLOADS, { withFileTypes: true });

for (const entry of files) {
const p = path.join(UPLOADS, entry.name);
const stat = await fs.stat(p);
const age = Date.now() – stat.mtimeMs;

if (age > maxAge) {
  await fs.rm(p, { recursive: entry.isDirectory(), force: true });
}

}
}

await cleanOldUploads(7);


Small note: I use `path.join` so Windows paths don’t trip me.

## When Node alone wasn’t enough

It worked most of the time. But I have two add-ons I like.

- rimraf (for stubborn folders or older Node versions)
```js
import { rimraf } from 'rimraf';

await rimraf('node_modules/.cache');

It cleans deep trees without stress. I used it on a CI runner that had weird perms.

  • trash (send to recycle bin instead of hard delete)
    “`js
    import trash from 'trash';

await trash(['./Desktop/test.txt']);

For user-facing apps, I don’t hard delete. I move to trash. People make mistakes. Me too.

## Little things that saved me

- Delete after closing files. Streams must end first, or Windows blocks you.
- Use `force: true` if missing files are normal. Less noise.
- Use `recursive: true` for folders. `unlink` won’t touch a folder.
- Handle paths with `path.join`. It keeps slashes right on every system.
- Check codes: `ENOENT` (not found), `EPERM/EBUSY` (locked), `EACCES` (no perms).

## A tiny CLI I keep around

This is my quick “nuke a path” script. I call it from npm scripts.

```js
#!/usr/bin/env node
import { rm } from 'node:fs/promises';

const target = process.argv[2];

if (!target) {
  console.error('Please pass a path: clean ./dist');
  process.exit(1);
}

try {
  await rm(target, { recursive: true, force: true });
  console.log(`Removed: ${target}`);
} catch (err) {
  console.error(`Failed: ${err.message}`);
  process.exit(2);
}

Package.json snippet:

{
  "scripts": {
    "prebuild": "node clean.mjs ./dist && node clean.mjs ./.cache"
  }
}

Fast and boring. I like boring.

What I loved, what bugged me

The good:

  • fs.rm felt simple. One API for files and folders.
  • force: true made my cleanup quiet.
  • Cross-platform worked, once I closed streams.

The meh:

  • Windows locks can be fussy.
  • Permissions on Linux can surprise you after Docker runs.
  • unlink vs rm