Node.js'in node:fs modülündeki her dosya sistemi işleminin üç biçimi vardır: senkron (readFileSync; değer döndürür, hatayı fırlatır, bitene kadar olay döngüsünü durdurur), callback (readFile(yol, cb); sonucu (err, data) ile verir) ve Promise (node:fs/promises içindeki readFile; await ile kullanılır). Üçü aynı işi yapar; fark, sonucun size nasıl ulaştığında, hataların nasıl yakalandığında ve olay döngüsünün bu sırada ne yaptığındadır. Uzun ömürlü sunucu kodunda Promise biçimi, tek seferlik betiklerde ve açılıştaki ayar okumalarında senkron biçim, en düşük ek yükün gerektiği yerlerde callback biçimi tercih edilir.
Bu yazıda üç biçimin içe aktarılmasını, çalışma sırasını, hata modelini, Promise API'sinin sayısal dosya tanımlayıcısı yerine kullandığı FileHandle nesnesini, await using ile otomatik kapatmayı, sayısal fd ile düşük düzeyli çalışmayı, eşzamanlı işlemlerin sıralama sorunlarını ve libuv iş parçacığı havuzunu Node.js v26.13.0 ile çalıştırılmış örneklerle anlatıyoruz. Tek tek işlemler için dosya okuma, dosya yazma ve klasör işlemleri yazılarımıza bakabilirsiniz.
Üç biçimi içe aktarmak

Promise biçimi node:fs/promises modülünden, callback ve senkron biçimler node:fs modülünden gelir. Belgedeki geçmiş kaydına göre Promise API v10.0.0'da eklendi, önce yalnızca require('fs').promises üzerinden erişilebiliyordu, v14.0.0 ile require('fs/promises') yolu açıldı. İki yol bugün aynı nesneyi verir. Callback alan herhangi bir fonksiyonu Promise döndürene çevirmek için util.promisify da kullanılabilir:
import fs from 'node:fs';
import fsp from 'node:fs/promises';
import { promisify } from 'node:util';
console.log(fs.promises === fsp);
const oku = promisify(fs.readFile);
console.log((await oku('ornek.txt', 'utf8')).slice(0, 7));
// çıktı: true
// çıktı: Node.jsfs fonksiyonları için hazır Promise biçimi olduğundan promisify'a genellikle gerek yoktur; Learn rehberi bunu yalnızca Promise API'sinin bulunmadığı eski sürümler için bir yol olarak anar. promisify ve callbackify'ın genel kullanımı util.promisify ve inspect yazımızda anlatılıyor. ESM ile CommonJS arasındaki içe aktarma farkları için CommonJS ve ESM farkı yazımıza bakabilirsiniz.
Çalışma sırası: kim önce biter?
Senkron çağrı, işlem bitene kadar hem olay döngüsünü hem de sonraki JavaScript satırlarını bekletir. Callback ve Promise çağrıları ise işi libuv iş parçacığı havuzuna bırakır ve kod hemen devam eder. Sonuç hazır olduğunda callback çağrılır ya da Promise çözülür:
import fs from 'node:fs';
import fsp from 'node:fs/promises';
const s = fs.readFileSync('ornek.txt', 'utf8');
console.log('1 sync:', s.length);
fs.readFile('ornek.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log('3 callback:', data.length);
});
fsp.readFile('ornek.txt', 'utf8').then((data) => {
console.log('3 promise:', data.length);
});
console.log('2 kod devam ediyor');
// çıktı: 1 sync: 17
// çıktı: 2 kod devam ediyor
// çıktı: 3 callback: 17
// çıktı: 3 promise: 17İlk iki satırın sırası kesindir. Son iki satırı beş çalıştırmada aynı sırada aldık ama bu sıraya güvenemezsiniz: belgenin "callback ve Promise tabanlı işlemlerin sırası" notuna göre bu işlemler iş parçacığı havuzunda asenkron yürütüldüğü için hangisinin önce biteceği garanti edilmez. Sıra önemliyse birini diğerinin sonucunu bekledikten sonra başlatmalısınız. Olay döngüsünün fazları ve kuyrukları için nextTick ve setImmediate yazımıza, dil düzeyindeki görev ve mikro görev kuyrukları için JavaScript event loop yazımıza bakabilirsiniz.
Sıralama hatası: rename ve stat
Belgedeki klasik örnek şudur: fs.rename() çağırıp hemen ardından, callback'ini beklemeden yeni ad için fs.stat() çağırırsanız stat işlemi rename'den önce bitebilir ve ENOENT alırsınız. Callback biçiminde çözüm ikinci çağrıyı ilkinin callback'i içine taşımak, Promise biçiminde ise her adımı await etmektir:
import { writeFile, rename, stat } from 'node:fs/promises';
await writeFile('a.txt', 'merhaba');
await rename('a.txt', 'b.txt');
const st = await stat('b.txt');
console.log(st.size);
// çıktı: 7await ile yazılan kod yukarıdan aşağı okunur ve her adım öncekinin bitmesini bekler; iç içe callback'lerin yarattığı girinti de oluşmaz. Promise biçiminin ekiplerde yaygınlaşmasının ana nedeni budur. Dil tarafı için async/await yazımıza ve Promise nedir yazımıza bakabilirsiniz.
Hata modeli: üç biçimde üç farklı yakalama

Üç biçimin hatayı iletme yolu farklıdır. Senkron biçim hatayı hemen fırlatır; try...catch ile yakalarsınız ya da yukarı doğru yayılmasına izin verirsiniz. Promise biçiminde Promise reddedilir; await ile kullandığınızda yine try...catch ile yakalanır. Callback biçiminde ise hata fırlatılmaz, callback'in ilk parametresi olarak gelir; başarı durumunda bu parametre null ya da undefined olur:
import { readFile } from 'node:fs';
readFile('yok.txt', (err, data) => {
console.log(err.code, data);
});
// çıktı: ENOENT undefinedimport { readFileSync } from 'node:fs';
try {
readFileSync('yok.txt');
} catch (err) {
console.log(err instanceof Error, err.code, err.errno !== undefined);
}
// çıktı: true ENOENT trueCallback biçiminin en sinsi tuzağı, callback içinde err kontrolünü unutmaktır: hata oluşsa bile kod çalışmaya devam eder ve data tanımsız olduğu için ilerideki bir satırda anlamsız bir hata alırsınız. Callback içinde throw etmek de çağıran koddaki try...catch tarafından yakalanmaz; o kod çoktan bitmiştir. Her üç biçimde de gelen nesne code, errno, syscall ve path alanlarını taşıyan bir sistem hatasıdır. Dil düzeyinde hata yakalama için try...catch yazımıza, yakalanmayan hataların süreci nasıl etkilediği için uncaughtException ve unhandledRejection yazımıza bakabilirsiniz.
FileHandle: Promise API'nin dosya nesnesi
Callback ve senkron API'de bir dosyayı open() ile açtığınızda sayısal bir dosya tanımlayıcısı (file descriptor, fd) alırsınız. Promise API'de ise fsPromises.open() bir FileHandle nesnesi döndürür. Bu nesne sayısal tanımlayıcıyı sarar (filehandle.fd), bir EventEmitter'dır ve read, write, stat, truncate, readFile, readLines, createReadStream gibi yöntemleri doğrudan üzerinde taşır. Aynı dosya üzerinde birden fazla işlem yapacaksanız dosyayı bir kez açıp handle üzerinden çalışmak, her işlemde dosyayı yeniden açmaktan daha düzenlidir:
import { open } from 'node:fs/promises';
import { Buffer } from 'node:buffer';
let fh;
try {
fh = await open('ornek.txt', 'r');
console.log(typeof fh.fd);
const { size } = await fh.stat();
const { bytesRead, buffer } = await fh.read(Buffer.alloc(4), 0, 4, size - 3);
console.log(bytesRead, buffer.toString('utf8', 0, bytesRead));
} finally {
await fh?.close();
}
// çıktı: number
// çıktı: 3 APIread(buffer, offset, length, position) çağrısında son parametre dosyadaki konumdur; 17 baytlık dosyada size - 3, yani 14. bayttan okumaya başladık. Dört bayt istememize rağmen dosyanın sonuna gelindiği için bytesRead 3 oldu. Belgeye göre position null ya da -1 ise okuma dosyanın o anki konumundan yapılır ve konum ilerler; negatif olmayan bir tam sayı verilirse dosyanın mevcut konumu değişmez. Eşzamanlı değişiklik yoksa bytesRead değerinin 0 olması dosya sonuna gelindiğini gösterir.
await using ile otomatik kapatma
FileHandle nesnesinin [Symbol.asyncDispose]() yöntemi v20.4.0 ve v18.18.0 ile geldi, v24.2.0'da deneysel olmaktan çıktı. Bu yöntem await using bildirimiyle çalışır: değişkenin tanımlandığı blok, ister normal ister bir hata ya da return ile bitsin, handle otomatik olarak kapatılır. try...finally yazmanıza gerek kalmaz:
import { open } from 'node:fs/promises';
async function ilkSatir(yol) {
await using fh = await open(yol);
for await (const satir of fh.readLines()) return satir;
}
console.log(await ilkSatir('ornek.txt'));
// çıktı: Node.js dosya APIFonksiyon ilk satırı bulur bulmaz return ile çıktı; await using bu çıkışta handle'ı kapattı. Aynı mekanizma geçici klasörler için mkdtempDisposable() ile de kullanılabilir; örneği klasör işlemleri yazımızda bulunuyor.
Sayısal dosya tanımlayıcısı (fd) ile çalışmak
Callback ve senkron API'de fs.open() ve fs.openSync() bir tam sayı döndürür. Belgenin açıklamasına göre POSIX sistemlerde çekirdek her süreç için açık dosyaların bir tablosunu tutar ve her açık dosyaya sayısal bir tanımlayıcı atar; Windows farklı ama kavramsal olarak benzer bir mekanizma kullanır ve Node.js bu farkı soyutlayarak her açık dosyaya sayısal bir tanımlayıcı verir. Bu tanımlayıcıyla fstat, read, write gibi işlemleri yaparsınız:
import { openSync, fstatSync, readSync, closeSync } from 'node:fs';
import { Buffer } from 'node:buffer';
const fd = openSync('ornek.txt', 'r');
try {
console.log(Number.isInteger(fd));
console.log(fstatSync(fd).size);
const buf = Buffer.alloc(7);
const n = readSync(fd, buf, 0, 7, 0);
console.log(n, buf.toString());
} finally {
closeSync(fd);
}
// çıktı: true
// çıktı: 17
// çıktı: 7 Node.jsİşletim sistemleri aynı anda açık olabilecek tanımlayıcı sayısını sınırlar. Belge, işi biten tanımlayıcının kapatılmamasının bir bellek sızıntısına ve sonunda uygulamanın çökmesine yol açacağını vurgular. Sınıra ulaşıldığında yeni dosya açma girişimleri EMFILE (çok fazla açık dosya) ile başarısız olur. Hata belgesi bunun özellikle çok sayıda dosyanın paralel açıldığı durumlarda ve süreç başına düşük sınırı olan sistemlerde (özellikle macOS) görüldüğünü, sınırı aynı kabukta ulimit -n 2048 ile yükseltebileceğinizi belirtir.
Paralel işlemler ve iş parçacığı havuzu
Promise biçimi birden fazla bağımsız işlemi aynı anda başlatmayı kolaylaştırır. Bir kısmının başarısız olabileceği durumlarda Promise.allSettled her sonucun durumunu ayrı ayrı verir:
import { readFile } from 'node:fs/promises';
const adlar = ['ornek.txt', 'ornek.txt', 'yok.txt'];
const sonuc = await Promise.allSettled(adlar.map((a) => readFile(a, 'utf8')));
console.log(sonuc.map((s) => s.status));
console.log(sonuc[2].reason.code);
// çıktı: [ 'fulfilled', 'fulfilled', 'rejected' ]
// çıktı: ENOENTBelgeye göre dosya izleyicileri dışındaki tüm callback ve Promise tabanlı fs API'leri libuv'un iş parçacığı havuzunu kullanır. CLI belgesi bu havuzun boyutunun sabit olduğunu ve varsayılan değerin 4 olduğunu söyler; aynı havuzu crypto.pbkdf2(), crypto.scrypt(), crypto.randomBytes(), dns.lookup() ve zlib API'leri de paylaşır. Havuzdaki işlerden biri uzun sürerse ilgisiz görünen diğer işler de yavaşlar. Boyut UV_THREADPOOL_SIZE ortam değişkeniyle artırılabilir; ancak belge bunu süreç içinden process.env.UV_THREADPOOL_SIZE ile ayarlamanın çalışacağının garanti olmadığını, çünkü havuzun kullanıcı kodundan önce oluşturulduğunu belirtir.
Bir de güvenlik notu: belge, Promise API işlemlerinin senkronize edilmediğini ve iş parçacığı güvenli olmadığını, aynı dosya üzerinde eşzamanlı değişiklik yapılırken veri bozulabileceğini vurgular. Paralellik farklı dosyalar için güvenlidir; aynı dosyaya yazan işlemleri sıraya koyun. Promise birleştiricileri için Promise.all, allSettled, race ve any yazımıza, ağır CPU işlerini ana iş parçacığından çıkarmak için worker_threads yazımıza bakabilirsiniz.
Performans ve seçim

Belge, en yüksek performansın (çalışma süresi ve bellek ayırma açısından) gerektiği durumlarda callback biçiminin Promise biçimine tercih edilebileceğini açıkça yazar. Bu, Promise biçiminin yavaş olduğu anlamına gelmez; çok sık çağrılan ve her mikrosaniyenin önemli olduğu kodda farkın hissedilebileceği anlamına gelir. Çoğu uygulama kodunda okunabilirlik ve hata yönetiminin sadeliği daha belirleyicidir.
Senkron biçim için kural basittir: olay döngüsünü durdurduğu için eşzamanlı istek karşılayan bir sunucunun istek işleyicilerinde kullanılmamalıdır. Uygulama açılırken bir kez yapılan işler (ayar dosyası okumak, gerekli bir klasörü oluşturmak), komut satırı araçları ve derleme betikleri ise senkron biçimin doğal kullanım alanıdır. Olay döngüsünü bloklamanın ölçülmesi ve teşhisi Node.js performans optimizasyonu yazımızda ele alınıyor.
| Ölçüt | Senkron (fs.xxxSync) | Callback (fs.xxx) | Promise (fs/promises) |
|---|---|---|---|
| Sonucun gelişi | Dönüş değeri | callback(err, sonuç) | Çözülen Promise |
| Hata | Fırlatılır, try/catch | İlk parametre err | Reddedilen Promise, try/catch ile await |
| Olay döngüsü | Bloklanır | Serbest | Serbest |
| Dosya nesnesi | Sayısal fd | Sayısal fd | FileHandle |
| Belgedeki not | İşlem bitene kadar JS durur | En yüksek performans için tercih | Okunabilir, await using desteği |
| Uygun yer | Betikler, açılış ayarları | Sıcak yollar, eski kod | Sunucu ve uygulama kodu |
Sık yapılan hatalar
- Callback ve Promise sonuçlarının sırasına güvenmek: belgeye göre sıra garanti değildir; bağımlı işlemleri birbirini bekleyecek şekilde yazın.
- await unutmak:
fsp.writeFile(...)satırının başındakiawaitunutulursa sonraki satır işlem bitmeden çalışır ve hata yakalanmaz. - Callback'te err kontrolünü atlamak: hata durumunda
datatanımsızdır; ilk satırdaif (err)kontrolü yapın. - FileHandle kapatmamak: otomatik kapatmaya güvenilmez;
finallyya daawait usingkullanın. - Aynı dosyaya paralel yazmak: Promise API işlemleri senkronize edilmez; aynı dosyaya yazanları sıraya koyun.
- Binlerce dosyayı aynı anda açmak:
EMFILEalabilirsiniz ve havuzu paylaşan diğer işler yavaşlar; işleri gruplar halinde işleyin. - Sunucu isteğinde senkron API kullanmak: her çağrı tüm istekleri bekletir.
Sık Sorulan Sorular
fs/promises ile fs.promises aynı şey mi?
Evet. require('node:fs').promises === require('node:fs/promises') ifadesi true verir. node:fs/promises yolu v14.0.0'dan beri vardır; yeni kodda doğrudan bu yolu içe aktarmak daha okunaklıdır.
Hangi biçimi kullanmalıyım?
Uygulama ve sunucu kodunda node:fs/promises ile async/await, tek seferlik betiklerde ve açılış aşamasında senkron biçim, en düşük ek yükün önemli olduğu sık çağrılan kodda callback biçimi. Bir projede tek bir biçimde karar kılmak kodun okunmasını kolaylaştırır.
FileHandle ile sayısal fd arasındaki fark nedir?
İkisi de açık bir dosyayı temsil eder. Sayısal fd callback ve senkron API ile kullanılır ve kapatma tamamen sizin sorumluluğunuzdadır. FileHandle Promise API'ye aittir, yöntemleri üzerinde taşır, bir EventEmitter'dır ve await using ile otomatik kapatılabilir. Belge FileHandle'ın sistem tarafından daha iyi yönetildiğini ama yine de kapatılması gerektiğini söyler.
Senkron fs fonksiyonları ne zaman kabul edilebilir?
Olay döngüsünde bekleyen başka iş yokken: uygulama başlarken bir kez okunan ayarlar, komut satırı araçları, derleme ve kurulum betikleri. Gelen istekleri karşılayan bir sunucuda istek başına senkron dosya işlemi yapmayın.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: Node.js öğrenme rehberi · Dosya okuma · Dosya yazma · util.promisify · async/await · Node.js performans