Tarayıcıda bir sunucuya HTTP isteği göndermenin bugünkü standart yolu fetch() fonksiyonudur. fetch(url) bir Promise döndürür; bu söz, sunucunun durum kodu ve başlıkları geldiği anda bir Response nesnesiyle yerine gelir. Gövdeyi okumak için ayrıca response.json() veya response.text() çağırırsınız. En önemli kural şudur: sunucu 404 ya da 500 döndürse bile söz reddedilmez, bu yüzden her istekte response.ok değerini kendiniz kontrol etmelisiniz.
Bu yazıda GET ve POST isteklerini, JSON ve form verisi göndermeyi, yanıtı doğru biçimde okumayı, hataları ayırt etmeyi ve AbortController ile AbortSignal.timeout() kullanarak istekleri iptal etmeyi adım adım göreceksiniz. Request, Response ve Headers nesnelerinin ayrıntıları için Request, Response ve Headers rehberine, başka bir alan adına istek atarken karşılaşacağınız kurallar için CORS yazısına bakabilirsiniz. Örneklerde async/await kullanılıyor; bu söz dizimi yeniyse önce async/await rehberini okumanızı öneririz.
fetch() nasıl çalışır?
fetch() hem pencere (window) hem de worker bağlamında genel (global) bir fonksiyon olarak bulunur. İlk argüman getirilecek kaynağı tanımlar: bir URL metni, metne dönüştürülebilen bir nesne (örneğin URL) ya da hazır bir Request nesnesi olabilir. İkinci argüman isteğe bağlıdır ve RequestInit adı verilen seçenekler nesnesidir: yöntem, başlıklar, gövde, iptal sinyali ve benzeri ayarlar burada verilir. Göreli bir URL verirseniz sayfada belgenin baseURI değerine, worker içinde ise worker'ın adresine göre çözülür.
İşlem iki aşamada ilerler. Birinci aşamada tarayıcı isteği gönderir ve sunucudan durum satırı ile başlıklar gelir gelmez söz yerine gelir; bu anda gövdenin tamamı henüz inmemiş olabilir. İkinci aşamada siz gövdeyi okuyan bir yöntem çağırırsınız ve bu yöntem de ayrı bir söz döndürür. Bu yüzden tipik bir fetch kodunda iki tane await görürsünüz: biri yanıt nesnesi, biri gövde için.
async function getData() {
const url = "https://example.org/products.json";
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
const result = await response.json();
console.log(result);
} catch (error) {
console.error(error.message);
}
}Bu kalıp MDN'nin temel örneğidir ve neredeyse her projede küçük değişikliklerle tekrar kullanılır: istek atılır, durum kontrol edilir, başarılıysa gövde JSON olarak çözülür, herhangi bir aşamadaki hata tek bir catch bloğunda yakalanır.
fetch() ne zaman reddeder, ne zaman reddetmez?

Yeni başlayanların en sık takıldığı nokta budur. fetch() sözü yalnızca isteğin kendisi başarısız olduğunda reddedilir. Sunucu bir HTTP yanıtı döndürdüyse, bu yanıt bir hata durum kodu taşısa bile söz yerine gelir. Aşağıdaki tablo MDN'deki istisna listesini özetler.
| Durum | Sonuç |
|---|---|
| Sunucu 200 döndürdü | Söz yerine gelir, ok true olur |
| Sunucu 404, 500 veya 504 döndürdü | Söz yine yerine gelir, ok false olur |
| Ağ hatası (bağlantı yok, DNS hatası) | TypeError ile reddedilir |
| URL geçersiz veya kullanıcı adı ve parola içeriyor | TypeError ile reddedilir |
| Seçeneklerde geçersiz bir değer var | TypeError ile reddedilir |
| İstek bir izin politikasıyla engellendi | TypeError ile reddedilir |
| AbortController ile iptal edildi | AbortError adlı DOMException ile reddedilir |
| AbortSignal.timeout süresi doldu | TimeoutError adlı DOMException ile reddedilir |
Response.ok özelliği durum kodu 200 ile 299 arasındaysa true, diğer her durumda false olur. Kodun kendisi status, açıklama metni statusText özelliğindedir. Aşağıdaki örnekte ağ kullanmadan, yanıtı doğrudan Response kurucusuyla oluşturuyoruz; böylece aynı özelliklerin nasıl davrandığını görebilirsiniz:
const yok = new Response("Sayfa bulunamadı", { status: 404, statusText: "Not Found" });
console.log(yok.ok); // false
console.log(yok.status); // 404
console.log(yok.statusText); // Not FoundDurum kodlarının hangi anlama geldiğini merak ediyorsanız HTTP durum kodları yazısında 2xx, 3xx, 4xx ve 5xx gruplarının tamamı açıklanıyor. Ağ hatası ile CORS engelini birbirinden ayırmak ise koddan mümkün değildir; bu konuyu DOMException ve güvenli bağlam hataları yazısında ayrıntılı ele alıyoruz.
Yanıt gövdesini okuma
Response nesnesi gövdeyi farklı biçimlerde okumak için birkaç yöntem sunar. Hepsi asenkrondur ve bir söz döndürür:
| Yöntem | Sözün değeri | Tipik kullanım |
|---|---|---|
| json() | JSON olarak çözülmüş JavaScript değeri | REST API yanıtları |
| text() | Metin (string) | HTML, CSV, düz metin |
| blob() | Blob nesnesi | Görsel, PDF, indirme |
| arrayBuffer() | ArrayBuffer | İkili veri işleme |
| bytes() | Uint8Array | Bayt dizisi olarak ikili veri |
| formData() | FormData | multipart veya urlencoded gövde |
Burada adı yanıltıcı olabilecek bir ayrıntı var: json() size JSON metni vermez; JSON metnini çözerek bir JavaScript nesnesi, dizi, sayı veya metin üretir. JSON'un kendisiyle ilgili kurallar için JSON.parse ve JSON.stringify yazısına bakabilirsiniz. Gövde bir akış (stream) olduğu için yalnızca bir kez okunabilir. İkinci kez okumaya çalışırsanız TypeError alırsınız; gövde geçerli JSON değilse json() sözü SyntaxError ile reddedilir:
const cevap = Response.json({ ad: "Ayşe", puan: 42 });
console.log(cevap.headers.get("content-type")); // application/json
const veri = await cevap.json();
console.log(veri.ad); // Ayşe
console.log(cevap.bodyUsed); // true
try {
await cevap.json();
} catch (hata) {
console.log(hata.name); // TypeError
}
const bozuk = new Response("{bozuk json");
try {
await bozuk.json();
} catch (hata) {
console.log(hata.name); // SyntaxError
}Response.json() statik yöntemi, verdiğiniz değeri JSON gövdesi olarak taşıyan ve Content-Type başlığı application/json olan bir yanıt üretir. Gerçek uygulamada bu yanıtı sunucu gönderir; örnekte yalnızca okuma davranışını göstermek için kullandık. Gövdeyi iki kez kullanmanız gerekiyorsa okumadan önce response.clone() çağırmalısınız; ayrıntılar Request ve Response yazısında.
Bir görseli indirip sayfada göstermek istediğinizde blob() ile URL.createObjectURL() birlikte kullanılır. Bu örnek sayfadaki bir <img> elemanına bağlı olduğu için yalnızca tarayıcıda çalışır:
const image = document.querySelector("img");
async function setImage() {
try {
const response = await fetch("flowers.jpg");
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
const blob = await response.blob();
image.src = URL.createObjectURL(blob);
} catch (e) {
console.error(e);
}
}Blob ve dosya nesneleriyle çalışmanın diğer yolları File API ve FileReader yazısında anlatılıyor.
Content-Type başlığını kontrol etmek

Bir API bazen JSON yerine bir hata sayfası (HTML) döndürür. Bu durumda json() çağrısı SyntaxError ile patlar ve hata mesajı asıl sorunu gizler. MDN, gövdeyi okumadan önce content-type başlığına bakmayı önerir. Aşağıdaki küçük yardımcı fonksiyon hem durum kodunu hem de içerik türünü denetler; denemek için üç farklı yanıtı yerel olarak oluşturduk:
async function jsonOku(response) {
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
const tur = response.headers.get("content-type");
if (!tur || !tur.includes("application/json")) {
throw new TypeError("Yanıt JSON değil");
}
return response.json();
}
for (const r of [
Response.json({ urun: "kalem" }),
new Response("hata", { status: 500 }),
new Response("<p>merhaba</p>", { headers: { "Content-Type": "text/html" } }),
]) {
try {
const sonuc = await jsonOku(r);
console.log("tamam:", JSON.stringify(sonuc));
} catch (hata) {
console.log(hata.name + ": " + hata.message);
}
}
// tamam: {"urun":"kalem"}
// Error: Response status: 500
// TypeError: Yanıt JSON değilGerçek kodda jsonOku(await fetch(url)) biçiminde kullanırsınız. Böylece yanıtı işleyen her yer aynı kontrolden geçer ve hata mesajları sorunun gerçek nedenini gösterir.
GET isteğiyle veri göndermek
fetch() varsayılan olarak GET isteği yapar. GET isteklerinin gövdesi olmaz; veri göndermek istiyorsanız sorgu dizesine (query string) eklersiniz. Değerleri elle birleştirmek yerine URLSearchParams kullanmak, Türkçe karakterleri ve boşlukları doğru biçimde kodlar:
const params = new URLSearchParams({ q: "çay bardağı", sayfa: 2 });
console.log(`https://example.org/ara?${params}`);
// https://example.org/ara?q=%C3%A7ay+barda%C4%9F%C4%B1&sayfa=2
// Tarayıcıda:
// const response = await fetch(`https://example.org/ara?${params}`);GET isteğine body eklemeye çalışırsanız istek hiç oluşturulmaz. Request kurucusu ve fetch() aynı parametreleri aldığı için bunu ağ kullanmadan deneyebilirsiniz:
try {
new Request("https://example.org/ara", { method: "GET", body: "x" });
} catch (hata) {
console.log(hata.name); // TypeError
}POST isteği: JSON, form ve dosya göndermek
Başka bir yöntem kullanmak için method seçeneğini, gönderilecek veri için body seçeneğini verirsiniz. Gövde olarak metin, ArrayBuffer, TypedArray, DataView, Blob, File, URLSearchParams, FormData veya ReadableStream kullanılabilir. Bunların dışındaki nesneler toString() ile metne çevrilir; düz bir nesneyi doğrudan verirseniz sunucuya [object Object] gider. Bu yüzden JSON göndermek için nesneyi önce JSON.stringify() ile metne dönüştürürsünüz:
const response = await fetch("https://example.org/post", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username: "example" }),
});Content-Type başlığını unutmak sık yapılan bir hatadır. Gövde metin olduğunda tarayıcı türü kendisi belirler ve bu tür JSON değildir. Aşağıdaki örnek, aynı gövdeyle başlıklı ve başlıksız iki istek oluşturup tarayıcının atadığı türü gösterir:
const a = new Request("https://example.org/post", {
method: "POST",
body: JSON.stringify({ username: "example" }),
});
console.log(a.headers.get("content-type")); // text/plain;charset=UTF-8
const b = new Request("https://example.org/post", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username: "example" }),
});
console.log(b.headers.get("content-type")); // application/jsonSunucunuz gövdeyi Content-Type değerine bakarak çözüyorsa, başlık text/plain kaldığında gövdeyi JSON olarak görmeyebilir. Öte yandan application/json türü, başka bir kökene giden isteklerde ön uçuş (preflight) isteğini tetikler. Bu ilişkiyi CORS rehberinde ayrıntılı açıklıyoruz.
Klasik form kodlamasıyla veri göndermek için URLSearchParams gövde olarak verilebilir. Bu durumda tür başlığı otomatik olarak atanır:
const c = new Request("https://example.org/post", {
method: "POST",
body: new URLSearchParams({ username: "example", password: "password" }),
});
console.log(c.headers.get("content-type")); // application/x-www-form-urlencoded;charset=UTF-8
console.log(await c.text()); // username=example&password=passwordDosya yüklemek ya da bir HTML formunun tüm alanlarını göndermek için FormData kullanılır. FormData gövdesinde Content-Type başlığını elle yazmamalısınız; tarayıcı multipart/form-data türünü sınır (boundary) değeriyle birlikte kendisi ekler. Form gönderme olayı, requestSubmit() ve form.elements konuları FormData ile form gönderme yazısında yer alıyor.
İsteği iptal etmek ve zaman aşımı
fetch() kendi başına bir zaman aşımı süresine sahip değildir. İsteği iptal edilebilir yapmak için bir AbortController oluşturur, onun signal özelliğini signal seçeneğine verirsiniz. controller.abort() çağrıldığında söz AbortError ile reddedilir. Yanıt gelmiş ama gövde henüz okunmamışsa, gövdeyi okuyan çağrı da aynı hatayla reddedilir.
const controller = new AbortController();
document.querySelector("#fetch").addEventListener("click", async () => {
try {
const response = await fetch("https://example.org/get", {
signal: controller.signal,
});
console.log(`Response: ${response.status}`);
} catch (e) {
console.error(`Error: ${e}`);
}
});
document.querySelector("#cancel").addEventListener("click", () => {
controller.abort();
});Belirli bir süre sonra otomatik iptal için AbortSignal.timeout(ms) kullanılır. Süre dolduğunda söz bu kez TimeoutError adlı bir DOMException ile reddedilir; bu sayede kullanıcının iptal etmesi ile zaman aşımını birbirinden ayırabilirsiniz. Birden fazla sinyali birleştirmek için AbortSignal.any() vardır. Aşağıdaki örnek, ağ kullanmadan sinyallerin hangi nedenle iptal edildiğini gösterir:
const zaman = AbortSignal.timeout(50);
await new Promise((r) => setTimeout(r, 120));
console.log(zaman.aborted, zaman.reason.name); // true TimeoutError
const kontrol = new AbortController();
kontrol.abort();
console.log(kontrol.signal.aborted, kontrol.signal.reason.name); // true AbortError
const birlesik = AbortSignal.any([new AbortController().signal, AbortSignal.timeout(30)]);
await new Promise((r) => setTimeout(r, 100));
console.log(birlesik.aborted, birlesik.reason.name); // true TimeoutErrorTarayıcıdaki kullanımda hatayı adına göre ayırırsınız:
try {
const res = await fetch("video.mp4", { signal: AbortSignal.timeout(5000) });
const result = await res.blob();
} catch (err) {
if (err.name === "TimeoutError") {
console.error("5 saniyede yanıt gelmedi");
} else if (err.name === "AbortError") {
console.error("İstek kullanıcı tarafından iptal edildi");
} else {
console.error(`Hata: ${err.name}, ${err.message}`);
}
}Bir sinyal yalnızca bir kez kullanılabilir: iptal edildikten sonra aynı sinyalle başlatılan her fetch() hemen reddedilir. Her yeni istek için yeni bir AbortController oluşturun. Node.js tarafında zaman aşımı ve dispatcher ayarları farklı konulardır; onlar için Node.js fetch rehberine bakın.
Kimlik bilgileri ve başka kökenlere istek
Fetch açısından kimlik bilgisi (credential) çerezler, TLS istemci sertifikaları ve Authorization başlığıdır. credentials seçeneğinin varsayılan değeri same-origin'dir: kimlik bilgileri yalnızca aynı kökene giden isteklerde gönderilir. omit hiç göndermez, include başka kökenlere de gönderir. include kullandığınızda sunucunun da Access-Control-Allow-Credentials başlığıyla buna izin vermesi ve Access-Control-Allow-Origin değerinde yıldız yerine sizin kökeninizi yazması gerekir.
mode seçeneği varsayılan olarak cors'tur; başka bir kökene giden istekler CORS kurallarına göre değerlendirilir. same-origin başka kökene isteği tamamen yasaklar, no-cors ise okunamayan (opak) bir yanıt döndürür. Bu seçeneklerin tamamı Request, Response ve Headers yazısında, çerezlerin teknik ayrıntıları document.cookie ve Set-Cookie yazısında, kimlik bilgili isteklerin güvenlik boyutu ise CSRF yazısında ele alınıyor.
Büyük yanıtları akış olarak okumak
Yanıt gövdesi aslında bir ReadableStream'dir. text() gibi yöntemler tüm gövdenin inmesini bekler; çok büyük bir metin dosyasında ise parçaları geldikçe işlemek isteyebilirsiniz. MDN'deki örnek, gövdeyi TextDecoderStream üzerinden geçirip asenkron döngüyle okur:
const response = await fetch("https://www.example.org/a-large-file.txt");
if (!response.ok) {
throw new Error(`Response status: ${response.status}`);
}
const stream = response.body.pipeThrough(new TextDecoderStream());
for await (const value of stream) {
console.log(value);
}Gövdeye bu şekilde doğrudan eriştiğinizde ham baytlarla çalışırsınız ve kod çözme işini sizin yapmanız gerekir. Bir akışa okuyucu bağlandığında akış kilitlenir; içinden veri okunduğunda ise bozulmuş (disturbed) sayılır. İki durumda da başka bir kod aynı gövdeyi okuyamaz.
Sayfa kapanırken istek göndermek
keepalive: true seçeneği, isteği başlatan sayfa kapansa bile tarayıcının isteği yarıda kesmemesini sağlar. Oturum sonunda analitik verisi göndermek için kullanılır ve navigator.sendBeacon()'a göre başka yöntemler kullanabilme, istek özelliklerini özelleştirebilme ve yanıta erişebilme avantajları vardır. MDN'ye göre keepalive isteklerinde gövde boyutu 64 kibibayt ile sınırlıdır. Sayfa yaşam döngüsü olayları için DOMContentLoaded ve sayfa yaşam döngüsü yazısına bakabilirsiniz.
Sık yapılan hatalar

- ok kontrolünü atlamak: 404 veya 500 yanıtı
catchbloğuna düşmez;response.okkontrolü olmadan hata sayfasını veri gibi işlersiniz. - Gövde sözünü beklememek:
response.json()de bir söz döndürür;awaitunutulursa değişkende veri yerine bekleyen bir söz olur. - Nesneyi JSON.stringify olmadan göndermek: düz nesne
toString()ile metne çevrilir ve sunucuya anlamsız bir metin gider. - Content-Type başlığını unutmak: JSON metni
text/plain;charset=UTF-8türüyle gider. - FormData ile Content-Type yazmak: elle yazılan başlık sınır değerini içermediği için sunucu gövdeyi ayrıştıramaz.
- Gövdeyi iki kez okumak: ikinci okuma
TypeErrorverir; gerekiyorsa önceclone()kullanın. - Aynı AbortController'ı tekrar kullanmak: bir kez iptal edilen sinyal sonraki istekleri de hemen iptal eder.
- CORS hatasını kodla çözmeye çalışmak: izin sunucu tarafında verilir; ayrıntılar CORS hataları yazısında.
Sık Sorulan Sorular
fetch() ile XMLHttpRequest arasında hangisini kullanmalıyım?
MDN, Fetch API'yi XMLHttpRequest'in modern ve daha esnek karşılığı olarak tanımlar: söz tabanlıdır, service worker ve CORS gibi özelliklerle bütünleşiktir. Yükleme ilerlemesini olaylarla izlemek gibi özel bir ihtiyacınız yoksa yeni kodda fetch() tercih edilir. Karşılaştırma için XMLHttpRequest ve fetch farkı yazısına bakabilirsiniz.
fetch() isteği neden 404 olmasına rağmen catch bloğuna düşmüyor?
Çünkü sunucu bir yanıt döndürmüştür ve ağ açısından istek başarılıdır. fetch() sözü yalnızca ağ hatası, geçersiz URL, geçersiz seçenek veya iptal gibi durumlarda reddedilir. Hata durum kodlarını yakalamak için if (!response.ok) throw ... satırını eklemeniz gerekir.
fetch() isteğine nasıl zaman aşımı eklerim?
signal: AbortSignal.timeout(5000) seçeneğini verin. Süre dolduğunda söz TimeoutError ile reddedilir. Hem kullanıcı iptali hem zaman aşımı istiyorsanız AbortSignal.any([controller.signal, AbortSignal.timeout(5000)]) ile iki sinyali birleştirebilirsiniz.
fetch() çerezleri otomatik gönderir mi?
Aynı kökene giden isteklerde evet, çünkü credentials varsayılanı same-origin'dir. Başka bir kökene çerez göndermek için credentials: "include" gerekir ve sunucunun CORS başlıklarıyla buna izin vermesi şarttır. Çerezin SameSite özelliği Strict veya Lax ise çerez yine de siteler arası isteklerde gönderilmez.
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: Web API öğrenme rehberi · Request, Response ve Headers · CORS nedir · XMLHttpRequest ve fetch farkı · FormData ile form gönderme · Node.js ile fetch