2026-08-19arayuz4 dk

Hata mesajları da bir arayüzdür: kullanıcıya ne yazmalı

Bir hata mesajı kullanıcının bir sonraki hamlesini belirlemiyorsa yazılmamış sayılır. Metnin üç parçası, gösterim yeri ve ham teknik hatayı ekrana basmanın bedeli.

Hata mesajı yazmak, arayüz işinin en sona bırakılan kısmı. Mutlu yol tasarlanıyor, boş durum bazen düşünülüyor, hata metni ise genellikle backend'den ne geliyorsa ekrana basılıyor. Sonuç, kullanıcının okuyup hiçbir şey yapamadığı bir kutu oluyor.

Bu yazının tek iddiası var: bir hata mesajı, kullanıcının bir sonraki hamlesini belirlemiyorsa yazılmamış sayılır.

Üç ayrı okuyucu var, üçü aynı metni okumamalı

Bir hata üç tarafa birden anlatılıyor ve bu üçü karıştırıldığında ortaya çıkan metin hiçbirine yaramıyor:

  • Kullanıcı — ne olduğunu değil, ne yapacağını öğrenmek istiyor.
  • Destek — olayı bulabilmek için bir tutamak istiyor (bir referans kodu).
  • Geliştirici — yığın izi, istek gövdesi, korelasyon kimliği istiyor. Bunların hiçbiri ekranda görünmemeli.

Pratikte bu, hata nesnesinin en az iki alan taşıması demek: kullanıcıya gösterilecek metin ve loglanacak teknik ayrıntı.

type AppError = {
  code: string;          // "STOK_YETERSIZ" — sabit, çevrilmez, aranabilir
  userMessage: string;   // kullanıcıya gösterilen
  action?: { label: string; run: () => void };
  ref: string;           // "a3f91c" — destek + log eşleşmesi
  cause?: unknown;       // yalnızca loga gider
};

code alanının sabit ve İngilizce/aranabilir olması önemli: kullanıcı metni değişince destek dokümanı bozulmasın diye. Aramada eşleşen şey kod olmalı, cümle değil.

Ham teknik metni ekrana basmak, bir karar değil, kararsızlıktır

Sahada en çok görülen kalıp:

catch (e) {
  toast.error(e.message);  // "Request failed with status code 500"
}

Bu satır kullanıcıya hiçbir şey söylemiyor ama bir şey söylediği izlenimi verdiği için sorun daha uzun süre fark edilmiyor. Ayrıca sızıntı riski var: ORM hataları tablo ve kolon adlarını, doğrulama kütüphaneleri iç şema alanlarını mesaja koyar.

Kural olarak: e.message ekrana hiç gitmez. Ekrana giden şey, bilinen hata kodlarından bir eşleme; eşleşme yoksa genel bir metin ve bir referans kodu.

const METIN: Record<string, string> = {
  STOK_YETERSIZ: "Bu üründen depoda yeterli adet yok.",
  OTURUM_BITTI: "Oturumunuz sona erdi.",
};

function kullaniciMetni(code: string, ref: string) {
  return METIN[code] ?? `Beklenmeyen bir hata oldu. Destek kodu: ${ref}`;
}

Metnin içinde üç parça olmalı

İşe yarayan bir hata metni şu üçünü taşıyor: ne olmadı, neden olmadı, şimdi ne yapılacak. Üçüncüsü olmadan yazılan metinler, kibar bir "olmadı"dan ibaret kalıyor.

Kötü İyi
Geçersiz giriş Vergi numarası 10 haneli olmalı, 9 hane girdiniz.
İşlem başarısız Fatura kaydedilmedi çünkü seri numarası bitti. Tanımlar > Seri ekranından yeni seri açın.
Bir hata oluştu Kaydedilemedi, bağlantı koptu. Yazdıklarınız duruyor — tekrar deneyin.
Yetkiniz yok Bu ekranı yalnızca yönetici görebiliyor. Erişim için yöneticinize başvurun.

Sağ sütundaki metinlerin ortak yanı, kullanıcının elinde kalan bir sonraki adımın olması. Adım kullanıcının elinde değilse — sunucu tarafında bir arıza gibi — bunu açıkça söylemek gerekiyor: "Bizim tarafımızda bir sorun var, tekrar denemenize gerek yok." Aksi halde kullanıcı aynı butona onlarca kez basıyor.

Suçlamayan ama sorumluluğu da devretmeyen bir dil

"Yanlış girdiniz", "hatalı işlem yaptınız" gibi kalıplar gereksiz. Ama diğer uca kaçıp her şeyi belirsiz bırakmak da işe yaramıyor: "bir sorun oluşmuş olabilir" cümlesi, kullanıcıya sorunun gerçekten olup olmadığını bile söylemiyor.

Pratik ölçüt: cümlenin öznesi. Kullanıcının düzeltebileceği bir durumda alanı özne yapın ("Vergi numarası 10 haneli olmalı"). Sistemin sorunu olduğunda kendi tarafınızı özne yapın ("Faturayı kaydedemedik"). Kullanıcıyı özne yapmak ("Yanlış vergi numarası girdiniz") ikisinde de gereksiz.

Hata nerede gösterilecek

Metin kadar konum belirleyici:

  • Alan hatası → alanın hemen altında, alan kırmızıya dönmüş halde. Form üstünde tek bir özet kutusu, uzun formlarda hangi alanın sorunlu olduğunu gizliyor.
  • İşlem hatası (kaydet, gönder) → işlemi başlatan butonun yanında ya da o bölümün içinde. Ekranın köşesinde 3 saniye durup kaybolan toast, kullanıcı başka yere bakıyorsa hiç görülmüyor.
  • Sayfa geneli hatası (veri çekilemedi) → içeriğin yerinde, tekrar deneme butonuyla. Boş liste göstermek, "veri yok" ile "veri gelmedi"yi aynı şeye çeviriyor.
  • Oturum/yetki → yönlendirmeyle birlikte, sebebini söyleyerek. Sessizce giriş ekranına atmak, kullanıcının yazdıklarını kaybettiği en sinir bozucu kalıp.

Kaybolan toast ile ilgili basit bir kural: kullanıcının bir şey yapması gerekiyorsa mesaj kendiliğinden kaybolmaz.

Yeniden denemeyi kullanıcıya bırakmayın

Ağ hatalarında "tekrar deneyin" yazmak, aslında yapılabilecek bir işi kullanıcıya devretmek oluyor. Geçici hatalar (bağlantı kopması, 502, 503) için kısa bir otomatik yeniden deneme, kullanıcının hiç görmeyeceği bir hata yaratıyor.

async function istekle<T>(fn: () => Promise<T>, kalan = 2): Promise<T> {
  try {
    return await fn();
  } catch (e) {
    if (kalan > 0 && geciciMi(e)) {
      await new Promise(r => setTimeout(r, 400 * (3 - kalan)));
      return istekle(fn, kalan - 1);
    }
    throw e;
  }
}

Buradaki tek dikkat noktası, yeniden denenen isteğin yan etkisiz ya da idempotent olması. Ödeme ve fatura oluşturma gibi isteklerde otomatik tekrar, sunucu tarafında bir idempotency anahtarı olmadan yapılmaz.

Kendi metinlerinizi denetlemenin kısa yolu

Ürününüzdeki hata metinlerini toplayıp her birine iki soru sorun: bunu okuyan kişi ne yapacağını biliyor mu, ve bu metin destek ekibine olayı bulduracak bir tutamak veriyor mu. İkisine de "hayır" gelen her satır, kullanıcı tarafında bir destek çağrısına dönüşüyor. Denetimi kolaylaştırmak için metinleri tek bir dosyada toplamak yeterli — dağınık throw new Error("...") çağrıları, bu listeyi hiç çıkaramamak demek.