Chrome Geliştirici Araçları'nda tahminleri yakalayın: Neden zordur ve nasıl daha iyi hale getirilir?

Eric Leese
Eric Leese

Web uygulamalarındaki istisnalarda hata ayıklama basit görünür: Bir sorun olduğunda yürütmeyi duraklatın ve sorunu inceleyin. Ancak JavaScript'in eşzamansız yapısı bu işlemi şaşırtıcı derecede karmaşık hale getirir. Chrome Geliştirme Araçları, umutlar ve asenkron işlevler arasında istisnalar oluştuğunda ne zaman ve nerede duraklatacağını nasıl bilebilir?

Bu gönderide, DevTools'un bir istisnanın kodunuzda daha sonra yakalanıp yakalanmayacağını tahmin etme özelliği olan yakalama tahmininin zorlukları ele alınmaktadır. Bu işlemin neden bu kadar zor olduğunu ve V8'de (Chrome'u destekleyen JavaScript motoru) yapılan son iyileştirmelerin bu işlemi daha doğru hale getirip daha sorunsuz bir hata ayıklama deneyimi sunduğunu inceleyeceğiz.

Yakalama tahmininin önemi 

Chrome Geliştirici Araçları'nda, kod yürütmeyi yalnızca yakalanmayan istisnalar için duraklatma ve yakalananları atlama seçeneğiniz vardır. 

Chrome DevTools, yakalanan veya yakalanmayan istisnalarda duraklatma için ayrı seçenekler sunar.

Arka planda, hata ayıklayıcı, bağlamı korumak için bir istisna oluştuğunda hemen durur. Bu, bir tahmindir. Çünkü şu anda, özellikle de senkronize olmayan senaryolarda istisnanın kodun ilerleyen kısımlarında yakalanıp yakalanmayacağını kesin olarak bilmek mümkün değildir. Bu belirsizlik, durma sorununa benzer şekilde program davranışını tahmin etmenin doğasında var olan zorluktan kaynaklanır.

Aşağıdaki örneği ele alalım: Hata ayıklayıcı nerede duraklatılmalıdır? (Yanıtı bir sonraki bölümde bulabilirsiniz.)

async function inner() {
  throw new Error(); // Should the debugger pause here?
}

async function outer() {
  try {
    const promise = inner();
    // ...
    await promise;
  } catch (e) {
    // ... or should the debugger pause here?
  }
}

Hata ayıklayıcıda istisnalarda duraklatma yapmak, çalışmayı kesintiye uğratabilir ve sık sık kesintiye neden olabilir ve bilinmeyen koda atlayabilir. Bu sorunu azaltmak için yalnızca yakalanmayan istisnalarda hata ayıklama yapmayı seçebilirsiniz. Bu istisnalar, gerçek hataları bildirme olasılığı daha yüksektir. Ancak bu, yakalama tahmininin doğruluğuna bağlıdır.

Yanlış tahminler can sıkıcı olabilir:

  • Yanlış negatifler (yakalanacak bir öğenin "yakalanmadı" olarak tahmin edilmesi). Hata ayıklayıcıda gereksiz duraklamalar.
  • Yanlış pozitifler (avlanmayan bir balık için "avlandı" sonucunu tahmin etme). Kritik hataları yakalama fırsatlarını kaçırmanız, beklenenler de dahil olmak üzere tüm istisnalarda hata ayıklamanıza neden olabilir.

Hata ayıklama kesintilerini azaltmanın bir diğer yöntemi, belirtilen üçüncü taraf kodundaki istisnalarda ara vermelerini önleyen yoksayma listesini kullanmaktır.  Ancak doğru yakalama tahmini burada yine de çok önemlidir. Üçüncü taraf kodundan kaynaklanan bir istisna kaçar ve kendi kodunuzu etkilerse bu istisnanın hata ayıklamasını yapmanız gerekir.

Zaman uyumsuz kodun işleyiş şekli

Promise'ler, async ve await ile diğer asenkron kalıplar, bir istisna veya ret işleminin, istisna atıldığında belirlenmesi zor olan bir yürütme yolu izleyebileceği senaryolara yol açabilir. Bunun nedeni, istisna meydana gelene kadar promises'ın beklenememesi veya catch işleyicilerinin eklenememesidir. Önceki örneğimize göz atalım:

async function inner() {
  throw new Error();
}

async function outer() {
  try {
    const promise = inner();
    // ...
    await promise;
  } catch (e) {
    // ...
  }
}

Bu örnekte outer() önce inner()'i çağırır ve inner() hemen bir istisna atar. Bu durumda hata ayıklayıcı, inner()'ün reddedilmiş bir promise döndüreceği ancak şu anda bu promise'i bekleyen veya başka bir şekilde işleyen hiçbir şey olmadığı sonucuna varabilir. Hata ayıklayıcı, outer()'ün muhtemelen bunu bekleyeceğini ve bunu mevcut try bloğunda yapacağını tahmin edebilir ve bu nedenle bunu ele alabilir ancak reddedilen söz döndürülüp await ifadesine ulaşılana kadar hata ayıklayıcı bundan emin olamaz.

Hata ayıklayıcı, yakalama tahminlerinin doğru olacağı konusunda herhangi bir garanti veremez ancak doğru tahminde bulunmak için yaygın kodlama kalıpları için çeşitli sezgisel yöntemler kullanır. Bu kalıpları anlamak için sözlerin nasıl çalıştığını öğrenmek faydalı olacaktır.

V8'de JavaScript Promise, üç durumdan birinde olabilecek bir nesne olarak temsil edilir: karşılandı, reddedildi veya beklemede. Bir promise yerine getirilmiş durumdaysa ve .then() yöntemini çağırırsanız yeni bir beklemedeki promise oluşturulur ve işleyiciyi çalıştıracak, ardından promise'i işleyicinin sonucuyla yerine getirilmiş olarak ayarlayacak veya işleyici bir istisna atarsa promise'i reddedilmiş olarak ayarlayacak yeni bir promise tepkisi görevi planlanır. Reddedilen bir promise üzerinde .catch() yöntemini çağırırsanız da aynı durum gerçekleşir. Aksine, reddedilen bir promise üzerinde .then() veya yerine getirilmiş bir promise üzerinde .catch() çağrısı yapıldığında, aynı durumdaki bir promise döndürülür ve işleyici çalıştırılmaz. 

Beklemede olan bir söz, her tepki nesnesinin bir yerine getirme işleyicisi veya ret işleyicisi (veya her ikisi) ve bir tepki sözü içerdiği bir tepki listesi içerir. Bu nedenle, bekleyen bir söz üzerinde .then() çağrısı yapıldığında, yerine getirilmiş bir işleyici içeren bir tepkinin yanı sıra .then()'ün döndüreceği tepki sözü için yeni bir bekleyen söz eklenir. .catch() çağrısı, reddetme işleyicisi içeren benzer bir tepki ekler. .then() işlevi iki bağımsız değişkenle çağrıldığında her iki işleyiciyle de bir tepki oluşturulur. .finally() işlevi çağrıldığında veya söz beklediğinde ise bu özellikleri uygulamaya özel yerleşik işlevler olan iki işleyiciyle bir tepki eklenir.

Beklemede olan söz sonunda yerine getirildiğinde veya reddedildiğinde, yerine getirilen tüm işleyicileri veya reddedilen tüm işleyicileri için tepki işleri planlanır. İlgili tepki taahhütleri güncellenir ve kendi tepki işlerini tetikleyebilir.

Örnekler

Aşağıdaki kodu ele alalım:

return new Promise(() => {throw new Error();})
    .then(() => console.log('Never happened'))
    .catch(() => console.log('Caught'));

Bu kodun üç farklı Promise nesnesi içerdiği açık olmayabilir. Yukarıdaki kod aşağıdaki koda eşdeğerdir:

const promise1 = new Promise(() => {throw new Error();});
const promise2 = promise1.then(() => console.log('Never happened'));
const promise3 = promise2.catch(() => console.log('Caught'));
return promise3;

Bu örnekte aşağıdaki adımlar uygulanır:

  1. Promise oluşturucusu çağrılır.
  2. Beklemede olan yeni bir Promise oluşturulur.
  3. Anonim işlev çalıştırılır.
  4. İstisna atılır. Bu noktada hata ayıklayıcının durup durmayacağına karar vermesi gerekir.
  5. Promise kurucusu bu istisnayı yakalar ve ardından promise'inin durumunu rejected olarak değiştirir. Değeri, oluşturulan hataya ayarlanır. promise1 içinde depolanan bu promise'i döndürür.
  6. promise1 rejected durumunda olduğu için .then() tepki işi planlamaz. Bunun yerine, aynı hatayla reddedilmiş durumda olan yeni bir promise (promise2) döndürülür.
  7. .catch(), sağlanan işleyiciyle bir tepki işi planlar ve promise3 olarak döndürülen yeni bir bekleyen tepki vaadi oluşturur. Bu noktada hata ayıklayıcı, hatanın ele alınacağını bilir.
  8. Tepki görevi çalıştırıldığında işleyici normal şekilde döndürülür ve promise3 durumunun fulfilled olarak değiştirilir.

Bir sonraki örnekte benzer bir yapı kullanılmış olsa da uygulama oldukça farklıdır:

return Promise.resolve()
    .then(() => {throw new Error();})
    .then(() => console.log('Never happened'))
    .catch(() => console.log('Caught'));

Bu, şuna eşdeğerdir:

const promise1 = Promise.resolve();
const promise2 = promise1.then(() => {throw new Error();});
const promise3 = promise2.then(() => console.log('Never happened'));
const promise4 = promise3.catch(() => console.log('Caught'));
return promise4;

Bu örnekte aşağıdaki adımlar uygulanır:

  1. fulfilled durumunda bir Promise oluşturulur ve promise1'de depolanır.
  2. İlk anonim işlevle bir tepki vaadi görevi planlanır ve (pending) tepki vaadi promise2 olarak döndürülür.
  3. promise2 öğesine, istek karşılayan bir işleyici ve promise3 olarak döndürülen tepkisi ile bir tepki eklenir.
  4. promise3 öğesine, reddedilen bir işleyici ve promise4 olarak döndürülen başka bir tepki vaadi içeren bir tepki eklenir.
  5. 2. adımda planlanan tepki görevi çalıştırılır.
  6. İşleyici istisna atar. Bu noktada hata ayıklayıcının durdurup durdurmayacağına karar vermesi gerekir. Şu anda işleyici, çalışan tek JavaScript kodunuzdur.
  7. Görev bir istisnayla sona erdiğinden, ilişkili tepki vaadi (promise2) reddedilmiş duruma ayarlanır ve değeri, oluşturulan hataya ayarlanır.
  8. promise2'te bir tepki olduğu ve bu tepkinin reddedilmiş işleyicisi olmadığı için tepki vaadi (promise3) de aynı hatayla rejected olarak ayarlanır.
  9. promise3 bir tepki aldığından ve bu tepkinin reddedilmiş bir işleyicisi olduğundan, bu işleyici ve tepkisi için bir söz tepkisi görevi planlanır (promise4).
  10. Bu tepki görevi çalıştırıldığında işleyici normal şekilde döndürülür ve promise4 durumunun "tamamlandı" olarak değiştirilir.

Yakalama tahmini yöntemleri

Yakalama tahmini için iki olası bilgi kaynağı vardır. Bunlardan biri çağrı yığınıdır. Bu, senkronize istisnalar için geçerlidir: Hata ayıklayıcı, istisna geri sarma kodunun yaptığı gibi çağrı yığınını tarayabilir ve try...catch bloğunda olduğu bir çerçeve bulursa durur. Söz dizimi oluşturucularında veya hiç askıya alınmamış olan asenkron işlevlerde reddedilen sözler ya da istisnalar için hata ayıklayıcı da çağrı yığınını kullanır ancak bu durumda tahmini her durumda güvenilir olmayabilir. Bunun nedeni, eşzamansız kodun en yakın işleyiciye istisna atma yerine reddedilmiş bir istisna döndürmesi ve hata ayıklayıcının, arayanın bu istisnayla ne yapacağı hakkında birkaç varsayım yapması gerekmesidir.

Öncelikle hata ayıklayıcı, döndürülen bir promise alan bir işlevin bu promise'i veya türetilmiş bir promise'i döndürme olasılığının yüksek olduğunu varsayar. Böylece, yığının üst kısmındaki asenkron işlevlerin bu promise'i bekleme şansı olur. İkinci olarak, hata ayıklayıcı, bir söz bir asynkron işleve döndürülürse önce bir try...catch bloğuna girmeden veya bu bloktan çıkmadan yakında bekleyeceğini varsayar. Bu varsayımların hiçbirinin doğru olduğu garanti edilmez ancak bunlar, asenkron işlevlere sahip en yaygın kodlama kalıpları için doğru tahminler yapmak için yeterlidir. Chrome 125 sürümünde başka bir sezgisel yöntem ekledik: Hata ayıklayıcı, çağrılan bir öğenin döndürülecek değerde .catch()'yi (veya iki bağımsız değişkenli .then()'yi ya da .then() veya .finally() çağrılarının ardından .catch() ya da iki bağımsız değişkenli .then()) çağırıp çağırmadığını kontrol eder. Bu durumda hata ayıklayıcı, bunların takip ettiğimiz vaaddeki yöntemler veya onunla ilgili bir yöntem olduğunu varsayar. Bu nedenle, ret yakalanır.

İkinci bilgi kaynağı, söz tepkileri ağacıdır. Hata ayıklayıcı, bir kök söz vermeyle başlar. Bazen bu, reject() yönteminin kısa süre önce çağrıldığı bir sözdür. Daha yaygın olarak, bir söz tepkisi işi sırasında bir istisna veya ret oluştuğunda ve çağrı yığınında bunu yakalayan hiçbir şey görünmediğinde hata ayıklayıcı, tepkiyle ilişkili sözden itibaren izleme yapar. Hata ayıklayıcı, bekleyen vaadin tüm tepkilerini inceler ve bunların reddi işleyicileri olup olmadığını kontrol eder. Tepkilerden hiçbiri bu koşulu karşılamıyorsa tepki vaadini inceler ve bu noktadan itibaren yinelemeli olarak iz sürer. Tüm tepkiler nihayetinde bir ret işleyicisine yol açarsa hata ayıklayıcı, söz verme reddedilmesini yakalanmış olarak kabul eder. Örneğin, .finally() çağrısı için yerleşik ret işleyicisi sayılmaz.

Söz tepkisi ağacı, bilgi varsa genellikle güvenilir bir bilgi kaynağı sağlar. Promise.reject() çağrısı veya Promise oluşturucusu ya da henüz hiçbir şey beklemeyen bir asynkron işlev gibi bazı durumlarda, izlenecek tepki olmaz ve hata ayıklayıcının yalnızca çağrı yığınına güvenmesi gerekir. Diğer durumlarda, söz tepki ağacı genellikle yakalama tahminini tahmin etmek için gerekli işleyicileri içerir ancak daha sonra istisna durumunu yakalanan yerine yakalanmayan veya tam tersi şekilde değiştirecek daha fazla işleyicinin eklenmesi her zaman mümkündür. Promise.all/any/race tarafından oluşturulanlar gibi, gruptaki diğer vaatlerin bir retin nasıl ele alınacağını etkileyebileceği vaatler de vardır. Bu yöntemler için hata ayıklayıcı, söz henüz beklemedeyse söz reddedilmesinin iletileceğini varsayar.

Aşağıdaki iki örneğe göz atın:

Av tahmini için iki örnek

Yakalanan istisnalarla ilgili bu iki örnek benzer görünse de oldukça farklı yakalama tahmini sezgileri gerektirir. İlk örnekte, çözülmüş bir söz oluşturulur, ardından .then() için bir istisna atacak bir tepki işi planlanır, ardından tepki sözüne bir ret işleyici eklemek için .catch() çağrılır. Tepki görevi çalıştırıldığında istisna atılır ve promise tepki ağacı, yakalama işleyicisini içerir. Böylece, yakalandığı tespit edilir. İkinci örnekte, promise, catch işleyici ekleme kodu çalıştırılmadan hemen önce reddedilir. Bu nedenle, promise'in tepki ağacında ret işleyici yoktur. Hata ayıklayıcı, çağrı yığınına bakmalıdır ancak try...catch bloğu da yoktur. Hata ayıklayıcı, bunu doğru bir şekilde tahmin etmek için .catch() çağrısını bulmak üzere koddaki mevcut konumun ilerisini tarar ve bu temelde reddedilmenin nihayetinde ele alınacağını varsayar.

Özet

Bu açıklamanın, Chrome Geliştirici Araçları'nda yakalama tahmininin işleyiş şekli, güçlü yönleri ve sınırlamaları hakkında bilgi verdiğini umuyoruz. Yanlış tahminler nedeniyle hata ayıklama sorunlarıyla karşılaşırsanız aşağıdaki seçenekleri değerlendirin:

  • Kodlama düzenini, tahmin edilmesi daha kolay bir şeye (ör. asynkron işlevler kullanma) değiştirin.
  • DevTools gerektiğinde durmuyorsa tüm istisnalarda duraklatmayı seçin.
  • Hata ayıklayıcı, istemediğiniz bir yerde duruyorsa "Burada hiçbir zaman duraklatma" kesme noktası veya koşullu kesme noktası kullanın.

Teşekkür ederiz

Bu yayını düzenleme konusundaki değerli yardımları için Sofia Emelianova ve Jecelyn Yeen'e çok teşekkür ederiz.