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.
The basic way: fs.unlink (it’s fine… until it isn’t)
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: truemeans “it’s okay if it’s gone already”recursive: trueis 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.
- 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 });
- 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.rmfelt simple. One API for files and folders.force: truemade 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.
unlinkvsrm
