Прогнозирование уловов в Chrome DevTools: почему это сложно и как это сделать лучше

Эрик Лиз
Eric Leese

Отладка исключений в веб-приложениях кажется простой: приостановите выполнение, если что-то пойдет не так, и изучите ситуацию. Но асинхронная природа JavaScript делает эту задачу удивительно сложной. Как Chrome DevTools может узнать, когда и где делать паузу, когда исключения возникают из-за промисов и асинхронных функций?

В этом посте рассматриваются проблемы прогнозирования перехвата — способность DevTools предвидеть, будет ли исключение перехвачено позже в вашем коде. Мы выясним, почему это так сложно и как недавние улучшения в V8 (движок JavaScript, лежащий в основе Chrome) делают его более точным, что приводит к более плавной отладке.

Почему прогнозирование ловли имеет значение

В Chrome DevTools у вас есть возможность приостановить выполнение кода только для неперехваченных исключений, пропуская перехваченные.

Chrome DevTools предоставляет отдельные параметры для приостановки при обнаружении или неперехвате исключений.

За кулисами отладчик немедленно останавливается при возникновении исключения, чтобы сохранить контекст. Это прогноз , потому что в данный момент невозможно точно знать, будет ли исключение перехвачено или нет позже в коде, особенно в асинхронных сценариях. Эта неопределенность проистекает из присущей сложности прогнозирования поведения программы, подобной проблеме остановки .

Рассмотрим следующий пример: где должен остановиться отладчик? (Ищите ответ в следующем разделе.)

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?
  }
}

Приостановка обработки исключений в отладчике может привести к частым прерываниям работы и переходам к незнакомому коду. Чтобы смягчить это, вы можете выбрать отладку только неперехваченных исключений, которые с большей вероятностью будут сигнализировать о реальных ошибках. Однако это зависит от точности прогнозирования улова.

Неправильные прогнозы приводят к разочарованию:

  • Ложноотрицательные результаты (предсказание «непойманности», когда она будет поймана) . Ненужные остановки в отладчике.
  • Ложные срабатывания (предсказание «пойма», когда он не будет «пойман») . Упущенные возможности отловить критические ошибки, потенциально вынуждающие вас отлаживать все исключения, включая ожидаемые.

Другой метод уменьшения прерываний отладки — использование списка игнорирования , который предотвращает прерывания исключений в указанном стороннем коде. Однако точный прогноз улова здесь по-прежнему имеет решающее значение. Если исключение, возникшее в стороннем коде, ускользает и влияет на ваш собственный код, вам понадобится возможность его отладки.

Как работает асинхронный код

Промисы, async и await и другие асинхронные шаблоны могут привести к сценариям, в которых исключение или отклонение перед обработкой может пройти путь выполнения, который трудно определить в момент создания исключения. Это связано с тем, что обещания могут не ожидаться или не добавлять обработчики перехвата до тех пор, пока исключение уже не произошло. Давайте посмотрим на наш предыдущий пример:

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

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

В этом примере метод outer() сначала вызывает inner() , который немедленно генерирует исключение. Из этого отладчик может сделать вывод, что inner() вернет отклоненное обещание, но в настоящее время ничего не ожидает или иным образом не обрабатывает это обещание. Отладчик может догадаться, что outer() вероятно, будет ожидать его, и догадаться, что он сделает это в своем текущем блоке try и, следовательно, обработает его, но отладчик не может быть уверен в этом до тех пор, пока не будет возвращено отклоненное обещание и в конечном итоге не будет достигнут оператор await .

Отладчик не может дать никаких гарантий, что прогнозы catch будут точными, но он использует различные эвристики для общих шаблонов кодирования для правильного прогнозирования. Чтобы понять эти закономерности, полезно узнать, как работают обещания.

В V8 Promise JavaScript представлено как объект, который может находиться в одном из трех состояний: выполнено, отклонено или ожидает выполнения. Если обещание находится в состоянии выполнения и вы вызываете метод .then() , создается новое ожидающее обещание и назначается новая задача реакции на обещание, которая запустит обработчик, а затем установит обещание выполненным с результатом обработчика или установит его как отклоненное, если обработчик выдает исключение. То же самое произойдет, если вы вызовете метод .catch() для отклоненного обещания. Напротив, вызов .then() для отклоненного обещания или .catch() для выполненного обещания вернет обещание в том же состоянии и не запустит обработчик.

Ожидающее обещание содержит список реакций, в котором каждый объект реакции содержит обработчик выполнения или обработчик отклонения (или оба) и обещание реакции. Таким образом, вызов .then() для ожидающего обещания добавит реакцию с выполненным обработчиком, а также новое ожидающее обещание для обещания реакции, которое .then() вернет. Вызов .catch() добавит аналогичную реакцию, но с обработчиком отклонения. Вызов .then() с двумя аргументами создает реакцию с обоими обработчиками, а вызов .finally() или ожидание обещания добавит реакцию с двумя обработчиками, которые являются встроенными функциями, специфичными для реализации этих функций.

Когда ожидающее обещание в конечном итоге будет выполнено или отклонено, задания реакции будут запланированы либо для всех его выполненных обработчиков, либо для всех его отклоненных обработчиков. Соответствующие обещания реагирования затем будут обновлены, что потенциально может вызвать собственные реакции.

Примеры

Рассмотрим следующий код:

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

Может быть неочевидно, что этот код включает в себя три различных объекта Promise . Приведенный выше код эквивалентен следующему коду:

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

В этом примере происходят следующие шаги:

  1. Вызывается конструктор Promise .
  2. Создается новое ожидающее Promise .
  3. Анонимная функция запускается.
  4. Выдается исключение. На этом этапе отладчик должен решить, остановиться или нет.
  5. Конструктор обещания перехватывает это исключение, а затем изменяет состояние своего обещания на rejected , при этом его значение устанавливается на возникшую ошибку. Он возвращает это обещание, которое хранится в promise1 .
  6. .then() не планирует никакого задания реагирования, поскольку promise1 находится в состоянии rejected . Вместо этого возвращается новый промис ( promise2 ), который также находится в отклоненном состоянии с той же ошибкой.
  7. .catch() планирует задание реакции с предоставленным обработчиком и новым ожидающим обещанием реакции, которое возвращается как promise3 . На этом этапе отладчик знает, что ошибка будет обработана.
  8. Когда задача реакции запускается, обработчик возвращается в обычном режиме, а состояние promise3 меняется на fulfilled .

Следующий пример имеет аналогичную структуру, но исполнение совершенно другое:

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

Это эквивалентно:

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;

В этом примере происходят следующие шаги:

  1. Promise создается в fulfilled состоянии и сохраняется в promise1 .
  2. Задача реакции на обещание запланирована с помощью первой анонимной функции, и ее (pending) обещание реакции возвращается как promise2 .
  3. Реакция добавляется к promise2 с выполненным обработчиком и обещанием реакции, которое возвращается как promise3 .
  4. Реакция добавляется к promise3 с отклоненным обработчиком и другим обещанием реакции, которое возвращается как promise4 .
  5. Запускается задача реагирования, запланированная на шаге 2.
  6. Обработчик выдает исключение. На этом этапе отладчик должен решить, остановиться или нет. В настоящее время обработчик — это ваш единственный работающий код JavaScript.
  7. Поскольку задача завершается исключением, соответствующее обещание реакции ( promise2 ) устанавливается в состояние отклонения, а его значение равно значению возникшей ошибки.
  8. Поскольку у promise2 была одна реакция, и эта реакция не имела обработчика отклонения, его обещание реакции ( promise3 ) также установлено как rejected с той же ошибкой.
  9. Поскольку у promise3 была одна реакция, и эта реакция имела отклоненный обработчик, задача реакции на обещание запланирована с этим обработчиком и его обещанием реакции ( promise4 ).
  10. Когда эта задача реакции запускается, обработчик возвращается в обычном режиме, и состояние promise4 меняется на выполненное.

Методы прогнозирования улова

Существует два потенциальных источника информации для прогнозирования улова. Одним из них является стек вызовов. Это нормально для синхронных исключений: отладчик может обходить стек вызовов так же, как это делает код обработки исключений, и останавливается, если находит кадр, в котором он находится в блоке try...catch . Для отклоненных обещаний или исключений в конструкторах обещаний или в асинхронных функциях, которые никогда не приостанавливались, отладчик также полагается на стек вызовов, но в этом случае его прогноз не может быть надежным во всех случаях. Это связано с тем, что вместо выдачи исключения ближайшему обработчику асинхронный код вернет отклоненное исключение, и отладчик должен сделать несколько предположений о том, что с ним будет делать вызывающая сторона.

Во-первых, отладчик предполагает, что функция, получившая возвращенное обещание, скорее всего, вернет это обещание или производное обещание, так что асинхронные функции, расположенные дальше по стеку, будут иметь возможность ожидать его. Во-вторых, отладчик предполагает, что если обещание будет возвращено асинхронной функции, она вскоре будет ожидать его, не входя и не выходя из блока try...catch . Ни одно из этих предположений не является гарантированно правильным, но их достаточно, чтобы сделать правильные прогнозы для наиболее распространенных шаблонов кодирования с асинхронными функциями. В версии Chrome 125 мы добавили еще одну эвристику: отладчик проверяет, собирается ли вызываемый объект вызвать .catch() для возвращаемого значения (или .then() с двумя аргументами, или цепочку вызовов .then() или .finally() за которыми следует .catch() или .then() с двумя аргументами). В этом случае отладчик предполагает, что это методы промиса, который мы отслеживаем, или методы, связанные с ним, поэтому отклонение будет перехвачено.

Второй источник информации — дерево реакций обещания. Отладчик начинается с корневого обещания. Иногда это промис, для которого только что был вызван метод reject() . Чаще всего, когда во время задания реакции на обещание происходит исключение или отклонение, и в стеке вызовов нет ничего, что могло бы его перехватить, отладчик отслеживает обещание, связанное с реакцией. Отладчик просматривает все реакции на ожидающее обещание и проверяет, есть ли у них обработчики отклонения. Если каких-либо реакций нет, он смотрит на обещание реакции и рекурсивно отслеживает его. Если все реакции в конечном итоге приводят к обработчику отклонения, отладчик считает, что отклонение обещания перехвачено. Есть некоторые особые случаи, которые следует рассмотреть, например, не считая встроенного обработчика отклонения для вызова .finally() .

Дерево реакций на обещания обычно обеспечивает надежный источник информации, если такая информация существует. В некоторых случаях, например, при вызове Promise.reject() , в конструкторе Promise или в асинхронной функции, которая еще ничего не ожидает, реакции на трассировку не будет, и отладчику придется полагаться только на стек вызовов. В других случаях дерево реакции обещания обычно содержит обработчики, необходимые для прогнозирования перехвата, но всегда возможно, что позже будут добавлены дополнительные обработчики, которые изменят исключение с перехваченного на неперехваченное или наоборот. Существуют также обещания, подобные тем, которые созданы Promise.all/any/race , где другие обещания в группе могут влиять на то, как обрабатывается отказ. Для этих методов отладчик предполагает, что отказ от обещания будет перенаправлен, если обещание все еще находится на рассмотрении.

Взгляните на следующие два примера:

Два примера прогнозирования улова

Хотя эти два примера перехваченных исключений выглядят одинаково, они требуют совершенно разных эвристик прогнозирования перехвата. В первом примере создается решенное обещание, затем запланировано задание реакции для .then() , которое выдаст исключение, затем вызывается .catch() , чтобы присоединить обработчик отклонения к обещанию реакции. При запуске задачи реагирования будет выброшено исключение, а дерево реакции обещания будет содержать обработчик catch, поэтому оно будет обнаружено как перехваченное. Во втором примере обещание немедленно отклоняется до запуска кода добавления обработчика перехвата, поэтому в дереве реакций обещания нет обработчиков отклонения. Отладчик должен просмотреть стек вызовов, но блоков try...catch там тоже нет. Чтобы правильно предсказать это, отладчик просматривает текущее место в коде, чтобы найти вызов .catch() , и на этом основании предполагает, что отклонение в конечном итоге будет обработано.

Краткое содержание

Надеемся, что это объяснение пролило свет на то, как прогнозирование перехвата работает в Chrome DevTools, на его сильные стороны и ограничения. Если вы столкнулись с проблемами отладки из-за неверных прогнозов, рассмотрите следующие варианты:

  • Измените шаблон кодирования на что-то более простое для прогнозирования, например, на использование асинхронных функций.
  • Выберите, чтобы прерывать работу по всем исключениям, если DevTools не может остановиться, когда это необходимо.
  • Используйте точку останова «Никогда не делать паузу здесь» или условную точку останова, если отладчик останавливается там, где вы этого не хотите.

Благодарности

Мы выражаем глубочайшую благодарность Софии Емельяновой и Джеселин Йин за неоценимую помощь в редактировании этого поста!

,

Эрик Лиз
Eric Leese

Отладка исключений в веб-приложениях кажется простой: приостановите выполнение, если что-то пойдет не так, и изучите ситуацию. Но асинхронная природа JavaScript делает эту задачу удивительно сложной. Как Chrome DevTools может узнать, когда и где делать паузу, когда исключения возникают из-за промисов и асинхронных функций?

В этом посте рассматриваются проблемы прогнозирования перехвата — способность DevTools предвидеть, будет ли исключение перехвачено позже в вашем коде. Мы выясним, почему это так сложно и как недавние улучшения в V8 (движок JavaScript, лежащий в основе Chrome) делают его более точным, что приводит к более плавной отладке.

Почему прогнозирование ловли имеет значение

В Chrome DevTools у вас есть возможность приостановить выполнение кода только для неперехваченных исключений, пропуская перехваченные.

Chrome DevTools предоставляет отдельные параметры для приостановки при обнаружении или неперехвате исключений.

За кулисами отладчик немедленно останавливается при возникновении исключения, чтобы сохранить контекст. Это прогноз , потому что в данный момент невозможно точно знать, будет ли исключение перехвачено или нет позже в коде, особенно в асинхронных сценариях. Эта неопределенность проистекает из присущей сложности прогнозирования поведения программы, подобной проблеме остановки .

Рассмотрим следующий пример: где должен остановиться отладчик? (Ищите ответ в следующем разделе.)

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?
  }
}

Приостановка обработки исключений в отладчике может привести к частым прерываниям работы и переходам к незнакомому коду. Чтобы смягчить это, вы можете выбрать отладку только неперехваченных исключений, которые с большей вероятностью будут сигнализировать о реальных ошибках. Однако это зависит от точности прогнозирования улова.

Неправильные прогнозы приводят к разочарованию:

  • Ложноотрицательные результаты (предсказание «непойманности», когда она будет поймана) . Ненужные остановки в отладчике.
  • Ложные срабатывания (предсказание «пойма», когда он не будет «пойман») . Упущенные возможности отловить критические ошибки, потенциально вынуждающие вас отлаживать все исключения, включая ожидаемые.

Другой метод уменьшения прерываний отладки — использование списка игнорирования , который предотвращает прерывания исключений в указанном стороннем коде. Однако точный прогноз улова здесь по-прежнему имеет решающее значение. Если исключение, возникшее в стороннем коде, ускользает и влияет на ваш собственный код, вам понадобится возможность его отладки.

Как работает асинхронный код

Промисы, async и await и другие асинхронные шаблоны могут привести к сценариям, в которых исключение или отклонение перед обработкой может пройти путь выполнения, который трудно определить в момент создания исключения. Это связано с тем, что обещания могут не ожидаться или не добавлять обработчики перехвата до тех пор, пока исключение уже не произошло. Давайте посмотрим на наш предыдущий пример:

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

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

В этом примере метод outer() сначала вызывает inner() , который немедленно генерирует исключение. Из этого отладчик может сделать вывод, что inner() вернет отклоненное обещание, но в настоящее время ничего не ожидает или иным образом не обрабатывает это обещание. Отладчик может догадаться, что outer() вероятно, будет ожидать его, и догадаться, что он сделает это в своем текущем блоке try и, следовательно, обработает его, но отладчик не может быть уверен в этом до тех пор, пока не будет возвращено отклоненное обещание и в конечном итоге не будет достигнут оператор await .

Отладчик не может дать никаких гарантий, что прогнозы catch будут точными, но он использует различные эвристики для общих шаблонов кодирования для правильного прогнозирования. Чтобы понять эти закономерности, полезно узнать, как работают обещания.

В V8 Promise JavaScript представлено как объект, который может находиться в одном из трех состояний: выполнено, отклонено или ожидает выполнения. Если обещание находится в состоянии выполнения и вы вызываете метод .then() , создается новое ожидающее обещание и назначается новая задача реакции на обещание, которая запустит обработчик, а затем установит обещание выполненным с результатом обработчика или установит его как отклоненное, если обработчик выдает исключение. То же самое произойдет, если вы вызовете метод .catch() для отклоненного обещания. Напротив, вызов .then() для отклоненного обещания или .catch() для выполненного обещания вернет обещание в том же состоянии и не запустит обработчик.

Ожидающее обещание содержит список реакций, в котором каждый объект реакции содержит обработчик выполнения или обработчик отклонения (или оба) и обещание реакции. Таким образом, вызов .then() для ожидающего обещания добавит реакцию с выполненным обработчиком, а также новое ожидающее обещание для обещания реакции, которое .then() вернет. Вызов .catch() добавит аналогичную реакцию, но с обработчиком отклонения. Вызов .then() с двумя аргументами создает реакцию с обоими обработчиками, а вызов .finally() или ожидание обещания добавит реакцию с двумя обработчиками, которые являются встроенными функциями, специфичными для реализации этих функций.

Когда ожидающее обещание в конечном итоге будет выполнено или отклонено, задания реакции будут запланированы либо для всех его выполненных обработчиков, либо для всех его отклоненных обработчиков. Соответствующие обещания реагирования затем будут обновлены, что потенциально может вызвать собственные реакции.

Примеры

Рассмотрим следующий код:

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

Может быть неочевидно, что этот код включает в себя три различных объекта Promise . Приведенный выше код эквивалентен следующему коду:

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

В этом примере происходят следующие шаги:

  1. Вызывается конструктор Promise .
  2. Создается новое ожидающее Promise .
  3. Анонимная функция запускается.
  4. Выдается исключение. На этом этапе отладчик должен решить, остановиться или нет.
  5. Конструктор обещания перехватывает это исключение, а затем изменяет состояние своего обещания на rejected , при этом его значение устанавливается на возникшую ошибку. Он возвращает это обещание, которое хранится в promise1 .
  6. .then() не планирует никакого задания реагирования, поскольку promise1 находится в состоянии rejected . Вместо этого возвращается новый промис ( promise2 ), который также находится в отклоненном состоянии с той же ошибкой.
  7. .catch() планирует задание реакции с предоставленным обработчиком и новым ожидающим обещанием реакции, которое возвращается как promise3 . На этом этапе отладчик знает, что ошибка будет обработана.
  8. Когда задача реакции запускается, обработчик возвращается в обычном режиме, а состояние promise3 меняется на fulfilled .

Следующий пример имеет аналогичную структуру, но исполнение совершенно другое:

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

Это эквивалентно:

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;

В этом примере происходят следующие шаги:

  1. Promise создается в fulfilled состоянии и сохраняется в promise1 .
  2. Задача реакции на обещание запланирована с помощью первой анонимной функции, и ее (pending) обещание реакции возвращается как promise2 .
  3. Реакция добавляется к promise2 с выполненным обработчиком и обещанием реакции, которое возвращается как promise3 .
  4. Реакция добавляется к promise3 с отклоненным обработчиком и другим обещанием реакции, которое возвращается как promise4 .
  5. Запускается задача реагирования, запланированная на шаге 2.
  6. Обработчик выдает исключение. На этом этапе отладчик должен решить, остановиться или нет. В настоящее время обработчик — это ваш единственный работающий код JavaScript.
  7. Поскольку задача завершается исключением, соответствующее обещание реакции ( promise2 ) устанавливается в состояние отклонения, а его значение равно значению возникшей ошибки.
  8. Поскольку у promise2 была одна реакция, и эта реакция не имела обработчика отклонения, его обещание реакции ( promise3 ) также установлено как rejected с той же ошибкой.
  9. Поскольку у promise3 была одна реакция, и эта реакция имела отклоненный обработчик, задача реакции на обещание запланирована с этим обработчиком и его обещанием реакции ( promise4 ).
  10. Когда эта задача реакции запускается, обработчик возвращается в обычном режиме, и состояние promise4 меняется на выполненное.

Методы прогнозирования улова

Существует два потенциальных источника информации для прогнозирования улова. Одним из них является стек вызовов. Это нормально для синхронных исключений: отладчик может обходить стек вызовов так же, как это делает код обработки исключений, и останавливается, если находит кадр, в котором он находится в блоке try...catch . Для отклоненных обещаний или исключений в конструкторах обещаний или в асинхронных функциях, которые никогда не приостанавливались, отладчик также полагается на стек вызовов, но в этом случае его прогноз не может быть надежным во всех случаях. Это связано с тем, что вместо выдачи исключения ближайшему обработчику асинхронный код вернет отклоненное исключение, и отладчик должен сделать несколько предположений о том, что с ним будет делать вызывающая сторона.

Во-первых, отладчик предполагает, что функция, получившая возвращенное обещание, скорее всего, вернет это обещание или производное обещание, так что асинхронные функции, расположенные дальше по стеку, будут иметь возможность ожидать его. Во-вторых, отладчик предполагает, что если обещание будет возвращено асинхронной функции, она вскоре будет ожидать его, не входя и не выходя из блока try...catch . Ни одно из этих предположений не является гарантированно правильным, но их достаточно, чтобы сделать правильные прогнозы для наиболее распространенных шаблонов кодирования с асинхронными функциями. В версии Chrome 125 мы добавили еще одну эвристику: отладчик проверяет, собирается ли вызываемый объект вызвать .catch() для возвращаемого значения (или .then() с двумя аргументами, или цепочку вызовов .then() или .finally() за которыми следует .catch() или .then() с двумя аргументами). В этом случае отладчик предполагает, что это методы промиса, который мы отслеживаем, или методы, связанные с ним, поэтому отклонение будет перехвачено.

Второй источник информации — дерево реакций обещания. Отладчик начинается с корневого обещания. Иногда это промис, для которого только что был вызван метод reject() . Чаще всего, когда во время задания реакции на обещание происходит исключение или отклонение, и в стеке вызовов нет ничего, что могло бы его перехватить, отладчик отслеживает обещание, связанное с реакцией. Отладчик просматривает все реакции на ожидающее обещание и проверяет, есть ли у них обработчики отклонения. Если каких-либо реакций нет, он смотрит на обещание реакции и рекурсивно отслеживает его. Если все реакции в конечном итоге приводят к обработчику отклонения, отладчик считает, что отклонение обещания перехвачено. Есть некоторые особые случаи, которые следует рассмотреть, например, не считая встроенного обработчика отклонения для вызова .finally() .

Дерево реакций на обещания обычно обеспечивает надежный источник информации, если такая информация существует. В некоторых случаях, например, при вызове Promise.reject() , в конструкторе Promise или в асинхронной функции, которая еще ничего не ожидает, реакции на трассировку не будет, и отладчику придется полагаться только на стек вызовов. В других случаях дерево реакции обещания обычно содержит обработчики, необходимые для прогнозирования перехвата, но всегда возможно, что позже будут добавлены дополнительные обработчики, которые изменят исключение с перехваченного на неперехваченное или наоборот. Существуют также обещания, подобные тем, которые созданы Promise.all/any/race , где другие обещания в группе могут влиять на то, как обрабатывается отказ. Для этих методов отладчик предполагает, что отказ от обещания будет перенаправлен, если обещание все еще находится на рассмотрении.

Взгляните на следующие два примера:

Два примера прогнозирования улова

Хотя эти два примера перехваченных исключений выглядят одинаково, они требуют совершенно разных эвристик прогнозирования перехвата. В первом примере создается решенное обещание, затем запланировано задание реакции для .then() , которое выдаст исключение, затем вызывается .catch() , чтобы присоединить обработчик отклонения к обещанию реакции. При запуске задачи реагирования будет выброшено исключение, а дерево реакции обещания будет содержать обработчик catch, поэтому оно будет обнаружено как перехваченное. Во втором примере обещание немедленно отклоняется до запуска кода добавления обработчика перехвата, поэтому в дереве реакций обещания нет обработчиков отклонения. Отладчик должен просмотреть стек вызовов, но блоков try...catch там тоже нет. Чтобы правильно предсказать это, отладчик просматривает текущее место в коде, чтобы найти вызов .catch() , и на этом основании предполагает, что отклонение в конечном итоге будет обработано.

Краткое содержание

Надеемся, что это объяснение пролило свет на то, как прогнозирование перехвата работает в Chrome DevTools, на его сильные стороны и ограничения. Если вы столкнулись с проблемами отладки из-за неверных прогнозов, рассмотрите следующие варианты:

  • Измените шаблон кодирования на что-то более простое для прогнозирования, например, на использование асинхронных функций.
  • Выберите, чтобы прерывать работу по всем исключениям, если DevTools не может остановиться, когда это необходимо.
  • Используйте точку останова «Никогда не делать паузу здесь» или условную точку останова, если отладчик останавливается там, где вы этого не хотите.

Благодарности

Мы выражаем глубочайшую благодарность Софии Емельяновой и Джеселин Йин за неоценимую помощь в редактировании этого поста!

,

Эрик Лиз
Eric Leese

Отладка исключений в веб-приложениях кажется простой: приостановите выполнение, если что-то пойдет не так, и изучите ситуацию. Но асинхронная природа JavaScript делает эту задачу удивительно сложной. Как Chrome DevTools может узнать, когда и где делать паузу, когда исключения возникают из-за промисов и асинхронных функций?

В этом посте рассматриваются проблемы прогнозирования перехвата — способность DevTools предвидеть, будет ли исключение перехвачено позже в вашем коде. Мы выясним, почему это так сложно и как недавние улучшения в V8 (движок JavaScript, лежащий в основе Chrome) делают его более точным, что приводит к более плавной отладке.

Почему прогнозирование ловли имеет значение

В Chrome DevTools у вас есть возможность приостановить выполнение кода только для неперехваченных исключений, пропуская перехваченные.

Chrome DevTools предоставляет отдельные параметры для приостановки при обнаружении или неперехвате исключений.

За кулисами отладчик немедленно останавливается при возникновении исключения, чтобы сохранить контекст. Это прогноз , потому что в данный момент невозможно точно знать, будет ли исключение перехвачено или нет позже в коде, особенно в асинхронных сценариях. Эта неопределенность проистекает из присущей сложности прогнозирования поведения программы, подобной проблеме остановки .

Рассмотрим следующий пример: где должен остановиться отладчик? (Ищите ответ в следующем разделе.)

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?
  }
}

Приостановка обработки исключений в отладчике может привести к частым прерываниям работы и переходам к незнакомому коду. Чтобы смягчить это, вы можете выбрать отладку только неперехваченных исключений, которые с большей вероятностью будут сигнализировать о реальных ошибках. Однако это зависит от точности прогнозирования улова.

Неправильные прогнозы приводят к разочарованию:

  • Ложноотрицательные результаты (предсказание «непойманности», когда она будет поймана) . Ненужные остановки в отладчике.
  • Ложные срабатывания (предсказание «пойма», когда он не будет «пойман») . Упущенные возможности отловить критические ошибки, потенциально вынуждающие вас отлаживать все исключения, включая ожидаемые.

Другой метод уменьшения прерываний отладки — использование списка игнорирования , который предотвращает прерывания исключений в указанном стороннем коде. Однако точный прогноз улова здесь по-прежнему имеет решающее значение. Если исключение, возникшее в стороннем коде, ускользает и влияет на ваш собственный код, вам понадобится возможность его отладки.

Как работает асинхронный код

Промисы, async и await и другие асинхронные шаблоны могут привести к сценариям, в которых исключение или отклонение перед обработкой может пройти путь выполнения, который трудно определить в момент создания исключения. Это связано с тем, что обещания могут не ожидаться или не добавлять обработчики перехвата до тех пор, пока исключение уже не произошло. Давайте посмотрим на наш предыдущий пример:

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

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

В этом примере метод outer() сначала вызывает inner() , который немедленно генерирует исключение. Из этого отладчик может сделать вывод, что inner() вернет отклоненное обещание, но в настоящее время ничего не ожидает или иным образом не обрабатывает это обещание. Отладчик может догадаться, что outer() вероятно, будет ожидать его, и догадаться, что он сделает это в своем текущем блоке try и, следовательно, обработает его, но отладчик не может быть уверен в этом до тех пор, пока не будет возвращено отклоненное обещание и в конечном итоге не будет достигнут оператор await .

Отладчик не может дать никаких гарантий, что прогнозы catch будут точными, но он использует различные эвристики для общих шаблонов кодирования для правильного прогнозирования. Чтобы понять эти закономерности, полезно узнать, как работают обещания.

В V8 Promise JavaScript представлено как объект, который может находиться в одном из трех состояний: выполнено, отклонено или ожидает выполнения. Если обещание находится в состоянии выполнения и вы вызываете метод .then() , создается новое ожидающее обещание и назначается новая задача реакции на обещание, которая запустит обработчик, а затем установит обещание выполненным с результатом обработчика или установит его как отклоненное, если обработчик выдает исключение. То же самое произойдет, если вы вызовете метод .catch() для отклоненного обещания. Напротив, вызов .then() для отклоненного обещания или .catch() для выполненного обещания вернет обещание в том же состоянии и не запустит обработчик.

Ожидающее обещание содержит список реакций, в котором каждый объект реакции содержит обработчик выполнения или обработчик отклонения (или оба) и обещание реакции. Таким образом, вызов .then() для ожидающего обещания добавит реакцию с выполненным обработчиком, а также новое ожидающее обещание для обещания реакции, которое .then() вернет. Вызов .catch() добавит аналогичную реакцию, но с обработчиком отклонения. Вызов .then() с двумя аргументами создает реакцию с обоими обработчиками, а вызов .finally() или ожидание обещания добавит реакцию с двумя обработчиками, которые являются встроенными функциями, специфичными для реализации этих функций.

Когда ожидающее обещание в конечном итоге будет выполнено или отклонено, задания реакции будут запланированы либо для всех его выполненных обработчиков, либо для всех его отклоненных обработчиков. Соответствующие обещания реагирования затем будут обновлены, что потенциально может вызвать собственные реакции.

Примеры

Рассмотрим следующий код:

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

Может быть неочевидно, что этот код включает в себя три различных объекта Promise . Приведенный выше код эквивалентен следующему коду:

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

В этом примере происходят следующие шаги:

  1. Вызывается конструктор Promise .
  2. Создается новое ожидающее Promise .
  3. Анонимная функция запускается.
  4. Выдается исключение. На этом этапе отладчик должен решить, остановиться или нет.
  5. Конструктор обещания перехватывает это исключение, а затем изменяет состояние своего обещания на rejected , при этом его значение устанавливается на возникшую ошибку. Он возвращает это обещание, которое хранится в promise1 .
  6. .then() не планирует никакого задания реагирования, поскольку promise1 находится в состоянии rejected . Вместо этого возвращается новый промис ( promise2 ), который также находится в отклоненном состоянии с той же ошибкой.
  7. .catch() планирует задание реакции с предоставленным обработчиком и новым ожидающим обещанием реакции, которое возвращается как promise3 . На этом этапе отладчик знает, что ошибка будет обработана.
  8. Когда задача реакции работает, обработчик возвращается нормально, и состояние promise3 изменяется на fulfilled .

Следующий пример имеет аналогичную структуру, но выполнение совершенно другое:

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

Это эквивалентно:

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;

В этом примере случаются следующие шаги:

  1. Promise создается в fulfilled состоянии и хранится в promise1 .
  2. Задача реакции обещания запланирована с первой анонимной функцией, и ее (pending) обещание реакции возвращается как promise2 .
  3. Реакция добавляется в promise2 с полноценным обработчиком и его реакционным обещанием, которое возвращается как promise3 .
  4. Реакция добавляется в promise3 с отвергнутым обработчиком и еще одним обещанием реакции, которое возвращается как promise4 .
  5. Задача реакции, запланированная на шаге 2, выполняется.
  6. Обработчик бросает исключение. На этом этапе отладчик должен решить, остановиться или нет. В настоящее время обработчик - ваш единственный запуск кода JavaScript.
  7. Поскольку задача заканчивается исключением, соответствующее реакционное обещание ( promise2 ) устанавливается в отклоненное состояние с его значением, устанавливаемой ошибкой, которая была брошена.
  8. Поскольку promise2 имел одну реакцию, и эта реакция не имела отвергнутого обработчика, его реакция обещания ( promise3 ) также rejected на то же самое.
  9. Поскольку promise3 имел одну реакцию, и эта реакция имела отвергнутый обработчик, задача реакции перспективы запланирована с этим обработчиком и его реакцией обещания ( promise4 ).
  10. Когда эта задача реакции работает, обработчик возвращается нормально, и состояние promise4 изменяется на выполнение.

Методы прогнозирования улова

Есть два потенциальных источника информации для прогнозирования улова. Один из них - стек вызовов. Это звучит для синхронных исключений: отладчик может ходить по стеку вызовов так же, как и исключение, что не будет и останавливается, если он найдет кадр, где он в try...catch блок. Для отклоненных обещаний или исключений в конструкторах перспектива или в асинхронных функциях, которые никогда не были приостановлены, отладчик также полагается на стек вызовов, но, в данном случае, его прогноз не может быть надежным во всех случаях. Это связано с тем, что вместо того, чтобы бросить исключение из ближайшего обработчика, асинхронный код вернет отклоненное исключение, и отладчик должен сделать несколько предположений о том, что абонент с ним сделает.

Во -первых, отладчик предполагает, что функция, которая получает возвращаемое обещание, может вернуть это обещание или полученное обещание, чтобы асинхронные функции дальше вверх в стеке будут иметь возможность ждать его. Во -вторых, отладчик предполагает, что, если обещание будет возвращено в асинхронную функцию, он скоро ожидает его, не введите или не оставив try...catch блок. Ни одно из этих допущений гарантированно не будет правильным, но их достаточно, чтобы сделать правильные прогнозы для наиболее распространенных моделей кодирования с асинхронными функциями. В Chrome версии 125 мы добавили еще одну эвристику: отладчик проверяет, если Callee собирается вызовать .catch() на значение, которое будет возвращено (или .then() .finally() с двумя аргументами или цепочкой вызовов к .then .catch() .then() или .then() с последующим. В этом случае отладчик предполагает, что это методы обещания, которое мы отслеживаем или связаны с ним, поэтому отказ будет пойман.

Вторым источником информации является дерево реакций обещания. Отладчик начинается с корневого обещания. Иногда это обещание, для которого только что был вызван его метод reject() . Чаще всего, когда исключение или отказ происходит во время работы реакции на обещание, и ничто в стеке вызовов не поймет его, отладчик отслеживает от обещания, связанного с реакцией. Отладчик рассматривает все реакции на ожидаемое обещание и видит, есть ли у него обработчики отказа. Если какие -либо реакции этого не делают, это рассматривает реакцию обещания и рекурсивно прослеживает от нее. Если все реакции в конечном итоге приводят к обработчику отказа, отладчик считает, что отказ от обещания будет пойман. Например, есть некоторые особые случаи, чтобы покрыть встроенный обработчик отклонения для вызова .finally() .

Дерево реакции обещания обеспечивает обычно надежный источник информации, если информация есть. В некоторых случаях, например, призыв Promise.reject() или в конструкторе Promise или в асинхронной функции, которая еще ничего не ожидала, не будет никаких реакций на трассировку, и отладчик должен полагаться только на стек вызовов. В других случаях дерево реакции перспектива обычно содержит обработчики, необходимые для вывода прогнозирования улова, но всегда возможно, что позже будет добавлено больше обработчиков, что изменит исключение с захвата на Uncaught или наоборот. Существуют также обещания, подобные тем, которые созданы Promise.all/any/race , где другие обещания в группе могут повлиять на то, как обрабатывается отторжение. Для этих методов отладчик предполагает, что отказ от обещания будет направлено, если обещание все еще находится на рассмотрении.

Взгляните на следующие два примера:

Два примера для прогнозирования улова

В то время как эти два примера пойманных исключений выглядят одинаково, они требуют совершенно другой эвристики прогнозирования. В первом примере создается разрешенное обещание, а затем запланировано реакционное задание для .then() , которое вызовет исключение, затем .catch() вызывается для прикрепления обработчика отклонения к обещанию реакции. Когда задача реакции будет выполнена, исключение будет брошено, и дерево реакции обещания будет содержать обработчик подвздоров, поэтому оно будет обнаружено как пойманное. Во втором примере обещание немедленно отклоняется до того, как код будет запущен, чтобы запустить обработчик улова, поэтому в дереве реакции обещания нет обработчиков отказа. Отладчик должен посмотреть на стек вызовов, но не try...catch блоки. Чтобы правильно предсказать это, отладчик сканирует перед текущим местоположением в коде, чтобы найти .catch() .

Краткое содержание

Надеемся, что это объяснение пролило свет на то, как работает прогноз улова в Chrome Devtools, его сильных сторонах и его ограничениях. Если вы сталкиваетесь с проблемами отладки из -за неправильных прогнозов, рассмотрите эти варианты:

  • Измените шаблон кодирования на что -то более простое, чтобы предсказать, например, использование асинхронных функций.
  • Выберите, чтобы сломать все исключения, если DevTools не остановится, когда это должно.
  • Используйте точку останова «никогда не паузу здесь» или условную точку останова, если отладчик останавливается где -то, что вы этого не хотите.

Благодарности

Наша самая глубокая благодарность вырабатывается Софии Эмельейновой и Джесели Иин за их бесценную помощь в редактировании этого поста!

,

Эрик Лиз
Eric Leese

Отладка исключений в веб -приложениях кажется простой: приостановите выполнение, когда что -то пойдет не так, и расследование. Но асинхронная природа JavaScript делает это удивительно сложным. Как Chrome Devtools знать, когда и где сделать паузу, когда исключения пролетают через обещания и асинхронные функции?

Этот пост погружается в проблемы прогнозирования улова - способность Devtools предвидеть, если исключение будет пойман позже в вашем коде. Мы рассмотрим, почему это так сложно и как недавние улучшения в V8 (javaScript Engine Powering Chrome) делают его более точным, что приводит к более плавному опыту отладки.

Зачем ловить прогноз

В Chrome Devtools у вас есть возможность приостановить выполнение кода только для исключений, пропуская той, которые поймали.

Chrome Devtools предоставляет отдельные варианты для приостановки на пойманных или непрерывных исключениях

За кулисами отладчик немедленно останавливается, когда происходит исключение, чтобы сохранить контекст. Это предсказание , потому что в данный момент невозможно точно знать, будет ли исключение поймано или нет позже в коде, особенно в асинхронных сценариях. Эта неопределенность связана с неотъемлемой сложностью прогнозирования поведения программы, аналогичной проблеме остановки .

Рассмотрим следующий пример: где должен сделать паузу отладчика? (Ищите ответ в следующем разделе.)

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?
  }
}

Приостановка исключений в отладчике может быть разрушительным и привести к частым перерывам и прыгает в незнакомый код. Чтобы смягчить это, вы можете отлаживать только непредубежные исключения, которые с большей вероятностью будут сигнализировать фактические ошибки. Тем не менее, это зависит от точности прогнозирования улова.

Неправильные прогнозы приводят к разочарованию:

  • Ложные негативы (прогнозирование «неада», когда он будет пойман) . Ненужные остановки в отладчике.
  • Ложные позитивы (прогнозирование «поймали», когда он будет непредубежден) . Упущенные возможности для того, чтобы поймать критические ошибки, потенциально заставляя вас отлаживать все исключения, в том числе ожидаемые.

Другим методом сокращения прерываний отладки является использование списка игнорирования , который предотвращает разрывы исключения в пределах указанного стороннего кода. Однако точный прогноз улова все еще имеет решающее значение здесь. Если исключение, возникающее в стороннем коде, ускользает от вашего собственного кода, вы захотите отладить его.

Как работает асинхронный код

Обещания, async и await , и другие асинхронные закономерности могут привести к сценариям, в которых исключение или отказ, прежде чем быть обработанным, могут выбрать путь выполнения, который трудно определить в то время, когда его исключение. Это связано с тем, что обещания не могут быть ожидаются или добавляют обработчики уловов до тех пор, пока исключение уже не произошло. Давайте посмотрим на наш предыдущий пример:

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

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

В этом примере outer() сначала вызывает inner() , который немедленно бросает исключение. Из этого отладчик может сделать вывод, что inner() вернет отклоненное обещание, но в настоящее время ничего не ожидает или иным образом обрабатывает это обещание. Отладчик может догадаться, что outer() , вероятно, ожидает этого, и предположить, что он сделает это в своем текущем блоке try и, следовательно, обрабатывает его, но отладчик не может быть уверен в этом до тех пор, пока не будет возвращено отвергнутое обещание и в конечном итоге будет достигнуто утверждение await .

Отладчик не может предложить каких -либо гарантий, что прогнозы улова будут точными, но он использует различные эвристики для правильного прогнозирования общих моделей кодирования. Чтобы понять эти модели, это помогает узнать, как работают обещания.

В V8 Promise JavaScript представлено как объект, который может быть в одном из трех состояний: выполнено, отвергнуто или в ожидании. Если обещание находится в исполнном состоянии, и вы называете метод .then() , создается новое ожидающее обещание, и запланировано новое задание реакции перспективы, которое запустит обработчик, а затем установит обещание выполнить с результатом обработчика или установить его, чтобы отклонить, если обработчик бросит исключение. То же самое происходит, если вы позвоните методу .catch() на отклоненном обещании. Напротив, вызов .then() на отклоненном обещании или .catch() на выполненном обещании вернет обещание в том же состоянии и не запустит обработчик.

Запрещающее обещание содержит список реакций, в котором каждый объект реакции содержит обработчик выполнения или обработчик отказа (или обоими) и обещание реакции. Таким образом, призыв .then() в ожидаемом обещании добавит реакцию с полноценным обработчиком, а также новое ожидающее обещание для реакции, которое вернется .then() . Призыв .catch() добавит аналогичную реакцию, но с обработчиком отказа. Призыв .then() с двумя аргументами создает реакцию с обоими обработчиками, и вызов .finally() или в ожидании обещания добавит реакцию с двумя обработчиками, которые являются встроенными функциями, специфичными для реализации этих функций.

Когда ожидаемое обещание в конечном итоге выполнено или отклонилось, реакционные задания будут запланированы либо для всех его выполненных обработчиков, либо для всех его отвергнутых обработчиков. Затем будут обновлены соответствующие обещания реакции, что может вызвать их собственные задачи реакции.

Примеры

Рассмотрим следующий код:

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

Может быть не очевидно, что этот код включает в себя три различных объекта Promise . Приведенный выше код эквивалентен следующему коду:

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

В этом примере случаются следующие шаги:

  1. Конструктор Promise называется.
  2. Новое ожидающее Promise создано.
  3. Анонимная функция запускается.
  4. Исключение брошено. На этом этапе отладчик должен решить, остановиться или нет.
  5. Конструктор перспективы выявляет это исключение, а затем меняет состояние его обещания rejected от его значения, установления от ошибки, которая была брошена. Он возвращает это обещание, которое хранится в promise1 .
  6. .then() не планирует реакцию, потому что promise1 находится в rejected состоянии. Вместо этого возвращается новое обещание ( promise2 ), которое также находится в отклоненном состоянии с той же ошибкой.
  7. .catch() планирует реакцию с предоставленным обработчиком и новым ожидающим реакцией, которое возвращается как promise3 . На этом этапе отладчик знает, что ошибка будет обработана.
  8. Когда задача реакции работает, обработчик возвращается нормально, и состояние promise3 изменяется на fulfilled .

Следующий пример имеет аналогичную структуру, но выполнение совершенно другое:

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

Это эквивалентно:

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;

В этом примере случаются следующие шаги:

  1. Promise создается в fulfilled состоянии и хранится в promise1 .
  2. Задача реакции обещания запланирована с первой анонимной функцией, и ее (pending) обещание реакции возвращается как promise2 .
  3. Реакция добавляется в promise2 с полноценным обработчиком и его реакционным обещанием, которое возвращается как promise3 .
  4. Реакция добавляется в promise3 с отвергнутым обработчиком и еще одним обещанием реакции, которое возвращается как promise4 .
  5. Задача реакции, запланированная на шаге 2, выполняется.
  6. Обработчик бросает исключение. На этом этапе отладчик должен решить, остановиться или нет. В настоящее время обработчик - ваш единственный запуск кода JavaScript.
  7. Поскольку задача заканчивается исключением, соответствующее реакционное обещание ( promise2 ) устанавливается в отклоненное состояние с его значением, устанавливаемой ошибкой, которая была брошена.
  8. Поскольку promise2 имел одну реакцию, и эта реакция не имела отвергнутого обработчика, его реакция обещания ( promise3 ) также rejected на то же самое.
  9. Поскольку promise3 имел одну реакцию, и эта реакция имела отвергнутый обработчик, задача реакции перспективы запланирована с этим обработчиком и его реакцией обещания ( promise4 ).
  10. Когда эта задача реакции работает, обработчик возвращается нормально, и состояние promise4 изменяется на выполнение.

Методы прогнозирования улова

Есть два потенциальных источника информации для прогнозирования улова. Один из них - стек вызовов. Это звучит для синхронных исключений: отладчик может ходить по стеку вызовов так же, как и исключение, что не будет и останавливается, если он найдет кадр, где он в try...catch блок. Для отклоненных обещаний или исключений в конструкторах перспектива или в асинхронных функциях, которые никогда не были приостановлены, отладчик также полагается на стек вызовов, но, в данном случае, его прогноз не может быть надежным во всех случаях. Это связано с тем, что вместо того, чтобы бросить исключение из ближайшего обработчика, асинхронный код вернет отклоненное исключение, и отладчик должен сделать несколько предположений о том, что абонент с ним сделает.

Во -первых, отладчик предполагает, что функция, которая получает возвращаемое обещание, может вернуть это обещание или полученное обещание, чтобы асинхронные функции дальше вверх в стеке будут иметь возможность ждать его. Во -вторых, отладчик предполагает, что, если обещание будет возвращено в асинхронную функцию, он скоро ожидает его, не введите или не оставив try...catch блок. Ни одно из этих допущений гарантированно не будет правильным, но их достаточно, чтобы сделать правильные прогнозы для наиболее распространенных моделей кодирования с асинхронными функциями. В Chrome версии 125 мы добавили еще одну эвристику: отладчик проверяет, если Callee собирается вызовать .catch() на значение, которое будет возвращено (или .then() .finally() с двумя аргументами или цепочкой вызовов к .then .catch() .then() или .then() с последующим. В этом случае отладчик предполагает, что это методы обещания, которое мы отслеживаем или связаны с ним, поэтому отказ будет пойман.

Вторым источником информации является дерево реакций обещания. Отладчик начинается с корневого обещания. Иногда это обещание, для которого только что был вызван его метод reject() . Чаще всего, когда исключение или отказ происходит во время работы реакции на обещание, и ничто в стеке вызовов не поймет его, отладчик отслеживает от обещания, связанного с реакцией. Отладчик рассматривает все реакции на ожидаемое обещание и видит, есть ли у него обработчики отказа. Если какие -либо реакции этого не делают, это рассматривает реакцию обещания и рекурсивно прослеживает от нее. Если все реакции в конечном итоге приводят к обработчику отказа, отладчик считает, что отказ от обещания будет пойман. Например, есть некоторые особые случаи, чтобы покрыть встроенный обработчик отклонения для вызова .finally() .

Дерево реакции обещания обеспечивает обычно надежный источник информации, если информация есть. В некоторых случаях, например, призыв Promise.reject() или в конструкторе Promise или в асинхронной функции, которая еще ничего не ожидала, не будет никаких реакций на трассировку, и отладчик должен полагаться только на стек вызовов. В других случаях дерево реакции перспектива обычно содержит обработчики, необходимые для вывода прогнозирования улова, но всегда возможно, что позже будет добавлено больше обработчиков, что изменит исключение с захвата на Uncaught или наоборот. Существуют также обещания, подобные тем, которые созданы Promise.all/any/race , где другие обещания в группе могут повлиять на то, как обрабатывается отторжение. Для этих методов отладчик предполагает, что отказ от обещания будет направлено, если обещание все еще находится на рассмотрении.

Взгляните на следующие два примера:

Два примера для прогнозирования улова

В то время как эти два примера пойманных исключений выглядят одинаково, они требуют совершенно другой эвристики прогнозирования. В первом примере создается разрешенное обещание, а затем запланировано реакционное задание для .then() , которое вызовет исключение, затем .catch() вызывается для прикрепления обработчика отклонения к обещанию реакции. Когда задача реакции будет выполнена, исключение будет брошено, и дерево реакции обещания будет содержать обработчик подвздоров, поэтому оно будет обнаружено как пойманное. Во втором примере обещание немедленно отклоняется до того, как код будет запущен, чтобы запустить обработчик улова, поэтому в дереве реакции обещания нет обработчиков отказа. Отладчик должен посмотреть на стек вызовов, но не try...catch блоки. Чтобы правильно предсказать это, отладчик сканирует перед текущим местоположением в коде, чтобы найти .catch() .

Краткое содержание

Надеемся, что это объяснение пролило свет на то, как работает прогноз улова в Chrome Devtools, его сильных сторонах и его ограничениях. Если вы сталкиваетесь с проблемами отладки из -за неправильных прогнозов, рассмотрите эти варианты:

  • Измените шаблон кодирования на что -то более простое, чтобы предсказать, например, использование асинхронных функций.
  • Выберите, чтобы сломать все исключения, если DevTools не остановится, когда это должно.
  • Используйте точку останова «никогда не паузу здесь» или условную точку останова, если отладчик останавливается где -то, что вы этого не хотите.

Благодарности

Наша самая глубокая благодарность вырабатывается Софии Эмельейновой и Джесели Иин за их бесценную помощь в редактировании этого поста!