W aplikacji Node.js szybko pojawia się potrzeba sprawdzenia, czy wskazany plik istnieje, zanim zostanie odczytany, nadpisany albo zwrócony przez endpoint. Najprostsze rozwiązanie to fs.existsSync(), ale w aplikacji webowej często lepiej użyć operacji asynchronicznej albo od razu spróbować odczytu i obsłużyć błąd. Pokażę, czym różnią się te podejścia, kiedy ich używać oraz jak uniknąć problemów ze ścieżkami i blokowaniem serwera.
Najważniejsze informacje o sprawdzaniu plików w Node.js
- fs.existsSync() zwraca wartość boolean i dobrze sprawdza się w krótkich skryptach.
- fs.promises.access() pozwala asynchronicznie sprawdzić dostępność pliku.
- fs.exists() jest przestarzałe i nie powinno być używane w nowych projektach.
- Przed późniejszym odczytem pliku lepiej wykonać odczyt od razu i obsłużyć błąd ENOENT.
- Operacje synchroniczne w obsłudze żądań HTTP mogą blokować event loop.
Najprostsze sprawdzenie przez existsSync
Jeżeli potrzebuję tylko odpowiedzi „jest” albo „nie ma”, najkrótszym rozwiązaniem jest funkcja existsSync() z wbudowanego modułu `node:fs`. Zwraca ona `true`, gdy podana ścieżka istnieje, oraz `false`, gdy nie istnieje.
import { existsSync } from 'node:fs';
const filePath = './data/config.json';
if (existsSync(filePath)) {
console.log('Plik istnieje');
} else {
console.log('Pliku nie znaleziono');
}Ta metoda sprawdza zarówno pliki, jak i katalogi. Jeśli muszę potwierdzić, że ścieżka wskazuje konkretnie na plik, samo `existsSync()` nie wystarczy. Wtedy używam `statSync()` i metody isFile().
import { statSync } from 'node:fs';
const path = './data/config.json';
if (statSync(path, { throwIfNoEntry: false })?.isFile()) {
console.log('Ścieżka wskazuje na plik');
} else {
console.log('Plik nie istnieje albo ścieżka prowadzi do katalogu');
}W praktyce korzystam z `existsSync()` głównie w skryptach uruchamianych lokalnie, podczas konfiguracji aplikacji albo przy ładowaniu ustawień podczas startu procesu. Nie wybieram go bez namysłu w kodzie obsługującym każde żądanie HTTP, ponieważ synchroniczna operacja zatrzymuje wykonywanie innych zadań przez event loop.
Asynchroniczne sprawdzanie w aplikacji webowej
Serwer Node.js najlepiej wykorzystuje swoje możliwości wtedy, gdy operacje wejścia i wyjścia nie blokują wątku wykonującego kod. Do asynchronicznego sprawdzenia dostępności pliku można użyć `fs.promises.access()`.
import { access } from 'node:fs/promises';
async function fileExists(filePath) {
try {
await access(filePath);
return true;
} catch {
return false;
}
}
const available = await fileExists('./public/logo.svg');
console.log(available);Funkcja `access()` nie zwraca `true` lub `false`. Gdy operacja się powiedzie, kończy się bez wyjątku, a gdy pliku nie ma albo proces nie ma odpowiednich uprawnień, trafiamy do bloku `catch`. To podejście dobrze pasuje do warstwy serwisowej, middleware oraz endpointów zwracających pliki.
Można też sprawdzić konkretny poziom dostępu. Stała `F_OK` oznacza samo istnienie, a `R_OK` i `W_OK` odpowiednio możliwość odczytu i zapisu.
import { access, constants } from 'node:fs/promises';
try {
await access('./uploads/report.pdf', constants.R_OK);
console.log('Plik można odczytać');
} catch (error) {
console.error('Plik nie istnieje albo nie jest dostępny');
}Trzeba jednak rozróżnić istnienie pliku od jego dostępności. Plik może istnieć, ale konto uruchamiające Node.js może nie mieć prawa do odczytu. W aplikacji webowej ten szczegół często tłumaczy sytuacje, w których ścieżka wygląda poprawnie, a odpowiedź serwera nadal kończy się błędem.
Kiedy lepiej od razu otworzyć lub odczytać plik
Najczęstszy błąd wygląda tak: najpierw sprawdzamy, czy plik istnieje, a chwilę później próbujemy go odczytać. Między tymi dwiema operacjami plik może zostać usunięty, zmieniony albo zablokowany. To klasyczny problem race condition, czyli sytuacja, w której wynik pierwszej operacji przestaje być aktualny przed wykonaniem drugiej.
Jeśli celem jest odczyt, zwykle piszę kod wprost i obsługuję błąd operacji końcowej.
import { readFile } from 'node:fs/promises';
async function loadConfig() {
try {
const content = await readFile('./data/config.json', 'utf8');
return JSON.parse(content);
} catch (error) {
if (error.code === 'ENOENT') {
return null;
}
throw error;
}
}W tym przykładzie `ENOENT` oznacza, że wskazany plik lub katalog nie istnieje. Inne błędy, na przykład brak uprawnień albo uszkodzenie danych JSON, są ponownie rzucane, bo nie powinny zostać cicho zamaskowane. Obsługuję tylko te błędy, które rzeczywiście potrafię zinterpretować.
Podobnie wygląda zapis. Nie sprawdzam wcześniej, czy plik istnieje, jeśli i tak chcę go utworzyć lub nadpisać. Dla operacji wymagającej utworzenia nowego pliku można zastosować tryb `wx`, który zgłosi błąd, gdy plik już istnieje.
import { writeFile } from 'node:fs/promises';
try {
await writeFile('./data/new-report.txt', 'Treść raportu', {
encoding: 'utf8',
flag: 'wx'
});
console.log('Plik został utworzony');
} catch (error) {
if (error.code === 'EEXIST') {
console.log('Plik już istnieje');
} else {
throw error;
}
}Takie podejście jest bezpieczniejsze niż para `exists()` plus `writeFile()`, bo decyzja o utworzeniu pliku odbywa się w ramach właściwej operacji. W kodzie produkcyjnym ta różnica ma większe znaczenie niż kilka dodatkowych wierszy.
Ścieżki, typy i błędy, które zmieniają wynik
Ścieżka względna zależy od katalogu procesu
Ścieżka `./data/file.txt` jest liczona względem bieżącego katalogu procesu, a niekoniecznie względem pliku JavaScript, w którym znajduje się kod. Dlatego aplikacja uruchomiona z innego miejsca może nagle „stracić” plik, który lokalnie działał bez problemu.
W projektach ESM często buduję ścieżkę na podstawie `import.meta.url`. Dzięki temu lokalizacja pliku nie zależy od miejsca, z którego uruchomiono polecenie `node`.
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { existsSync } from 'node:fs';
const currentFile = fileURLToPath(import.meta.url);
const currentDirectory = path.dirname(currentFile);
const configPath = path.join(currentDirectory, 'data', 'config.json');
console.log(existsSync(configPath));W aplikacji serwerowej ścieżki pochodzące od użytkownika trzeba dodatkowo walidować. Samo sprawdzenie istnienia nie chroni przed próbą odwołania do niepożądanych lokalizacji, na przykład przez fragment `../`. Kontrola ścieżki i uprawnień jest osobnym zadaniem bezpieczeństwa.
Plik, katalog i dowiązanie symboliczne
`existsSync()` odpowiada na pytanie, czy coś znajduje się pod daną ścieżką, ale nie określa rodzaju obiektu. Gdy potrzebuję rozróżnić plik i katalog, korzystam z `stat()` lub `lstat()`. Pierwsze zwykle podąża za dowiązaniem symbolicznym, a drugie pozwala sprawdzić samo dowiązanie.
import { stat } from 'node:fs/promises';
try {
const info = await stat('./uploads');
if (info.isDirectory()) {
console.log('To jest katalog');
} else if (info.isFile()) {
console.log('To jest plik');
}
} catch (error) {
if (error.code === 'ENOENT') {
console.log('Ścieżka nie istnieje');
} else {
throw error;
}
}Przeczytaj również: Blazor Server w .NET - kiedy warto wybrać interaktywność serwerową?
Brak pliku to nie zawsze jedyny problem
Warto odróżnić kilka typowych kodów błędów. `ENOENT` oznacza brak ścieżki, `EACCES` lub `EPERM` wskazuje na problem z uprawnieniami, a `EISDIR` może pojawić się wtedy, gdy kod próbuje czytać katalog jak zwykły plik. Taka klasyfikacja daje użytkownikowi lepszy komunikat i ułatwia diagnozowanie wdrożenia.
| Metoda | Charakterystyka | Najlepsze zastosowanie |
|---|---|---|
existsSync() |
Synchroniczna, zwraca boolean | Skrypty i kod wykonywany podczas startu |
fs.promises.access() |
Asynchroniczna, zgłasza wyjątek przy problemie | Samo sprawdzenie istnienia lub uprawnień |
fs.promises.stat() |
Asynchroniczna, zwraca informacje o obiekcie | Rozróżnienie pliku, katalogu i metadanych |
readFile() lub writeFile()
|
Wykonuje docelową operację i obsługuje jej wynik | Gdy sprawdzenie jest tylko etapem przed odczytem lub zapisem |
Jak wybrać metodę do konkretnego scenariusza
Dobór rozwiązania jest prostszy, gdy najpierw określę, co ma się wydarzyć po sprawdzeniu. Samo pytanie o istnienie pliku wymaga innej metody niż wczytanie konfiguracji, wysłanie obrazu do klienta czy bezpieczne utworzenie raportu.
- Do prostego warunku w skrypcie używam existsSync().
- Do asynchronicznego sprawdzenia istnienia wybieram fs.promises.access().
- Do rozpoznania typu ścieżki korzystam z stat() i `isFile()` albo `isDirectory()`.
- Przed odczytem pliku zwykle pomijam osobny test i obsługuję ENOENT z `readFile()`.
- Przy tworzeniu nowego pliku stosuję odpowiedni tryb zapisu, na przykład flag: 'wx'.
Nie używam asynchronicznego `fs.exists()`, mimo że można go znaleźć w starszych przykładach. Ta funkcja jest oznaczona jako przestarzała, a jej callback różni się od typowego wzorca Node.js. W nowych projektach lepiej trzymać się API opartego na `promises`, `access()` i bezpośredniej obsłudze błędów.
W endpointach, które obsługują wiele żądań, szczególnie ważne jest unikanie synchronicznych operacji na dużych plikach. Jednorazowe sprawdzenie podczas uruchamiania serwera raczej nie będzie problemem, ale ten sam kod wykonywany przy każdym żądaniu może pogorszyć czas odpowiedzi pod obciążeniem.
Mały test pliku, duża różnica w stabilności aplikacji
Najkrótsza odpowiedź brzmi: użyj `existsSync()`, gdy potrzebujesz prostego synchronicznego sprawdzenia, oraz `fs.promises.access()`, gdy kod ma działać asynchronicznie. Jeżeli po sprawdzeniu i tak odczytujesz albo zapisujesz plik, najczęściej rozsądniej wykonać tę operację od razu i poprawnie obsłużyć jej błąd.
Właśnie takie drobne decyzje odróżniają działający przykład od kodu, który dobrze zachowuje się w aplikacji webowej. Stabilna implementacja uwzględnia nie tylko istnienie ścieżki, ale też jej typ, uprawnienia, sposób wyznaczania lokalizacji oraz możliwość, że stan systemu plików zmieni się między dwiema operacjami.
