Mateen Kiani
Published on Sun Jun 22 2025·4 min read
We all know that working with the file system is a core part of many Node.js applications, from simple scripts to complex servers. Yet developers often overlook the nuances of checking whether a file exists before accessing it. What’s the best way to perform this check without introducing bugs or blocking the event loop?
By understanding the differences between synchronous, callback-based, and promise-based approaches, you can choose the right method for your project. Let’s explore the options so you can make informed decisions, avoid surprises under load, and keep your code clean and reliable.
The simplest way to see if a file exists is with the synchronous method fs.existsSync
. It returns a boolean and halts execution until it finishes. For small scripts or startup checks, this can be fine. But in a busy server, it can block other requests.
const fs = require('fs')const path = './data/config.json'if (fs.existsSync(path)) {console.log('Config file found')} else {console.warn('Config file missing')}
Practical tips:
existsSync
at startup or in CLI tools.try/catch
if you expect permission errors.By limiting sync checks to non-critical paths, you avoid clogging your server’s event loop.
For non-blocking code, Node.js offers fs.access
, which takes a callback. It uses flags such as fs.constants.F_OK
to test for existence without reading data. This is ideal in request handlers or background jobs.
const fs = require('fs')const file = './logs/app.log'fs.access(file, fs.constants.F_OK, err => {if (err) {console.error('Log file does not exist')} else {console.log('Log file is present')}})
Tip: Always check the
err.code
property for granular control (e.g., 'ENOENT').
Use cases:
Modern Node.js supports promises in the fs.promises
API. This fits nicely with async/await
, making code cleaner.
const fs = require('fs').promisesasync function checkFile(path) {try {await fs.access(path)console.log('File exists')} catch {console.log('File not found')}}checkFile('./data/users.json')
Advantages:
Here’s a quick comparison to help you choose:
Method | Type | Blocking? | Example |
---|---|---|---|
fs.existsSync | Synchronous | Yes | fs.existsSync(path) |
fs.access (callback) | Asynchronous | No | fs.access(path, cb) |
fs.promises.access | Promise | No | await fs.promises.access(path) |
If you need to list files first, check this guide: list files in a directory.
Even with the right method, mistakes can sneak in. Keep these tips in mind:
exists
check before reading; the file might change immediately after.Best practice: Combine
try/catch
with clear error messages to simplify debugging.
Imagine an API that allows clients to fetch profile images. You need to check if the image file exists before streaming it.
userId
from request parameters.const filePath = ./images/${userId}.png
.fs.promises.access
in an async
handler.res.sendFile(filePath)
.404
and a friendly message.const fs = require('fs').promisesapp.get('/api/avatar/:id', async (req, res) => {const img = `./images/${req.params.id}.png`try {await fs.access(img)res.sendFile(img, { root: __dirname })} catch {res.status(404).json({ error: 'Avatar not found' })}})
This approach keeps your API fast, non-blocking, and user-friendly.
Checking if a file exists may seem trivial, but choosing the right method can have a big impact on performance and reliability. Use fs.existsSync
for quick startup checks, fs.access
callbacks for non-blocking code, and fs.promises.access
when you want clean async/await
flows. Remember the pitfalls—race conditions, permission errors, and blocking calls—and apply best practices like clear error handling and caching.
Armed with these patterns, you can confidently guard your file operations and build robust Node.js applications.
Meta description: Check if a file exists in Node.js using fs.existsSync, fs.access, or fs.promises.access with code examples and best practices.