Неполная трассировка стека при броске с асин c улова - PullRequest
3 голосов
/ 14 января 2020

Почему нет асинхронной c трассировки стека при повторном отбрасывании асинхронного исключения? С узлом 12+ исключение при запуске следующий код :

async function crash() {
    try {
        await (async () => {throw new Error('dead');})();
    } catch (e) {
        throw new Error('rethrow');
    }
}

async function foo() {
    await new Promise(resolve => setTimeout(() => resolve(), 1));
    await crash();
}

async function entrypoint() {
    try {
        await foo();
    } catch(e) {
        console.log(e.stack);
    }
}
entrypoint();

ужасно неполно:

Error: rethrow
    at crash (/async-stackt/crash.js:6:15)

Я нашел обходной путь , определив исключение в начале crash(), которое приводит к гораздо более хорошему:

Error: rethrow
    at crash (/workaround.js:2:17)
    at foo (/workaround.js:12:11)
    at async entrypoint (/workaround.js:17:9)

Это не оптимально, поскольку ошибка должна быть заранее составлена ​​независимо от того, нужна она или нет, и трассировка стека несколько неточна.

Почему трассировка стека неполная при выдаче ошибки из асинхронного c блока catch? Есть ли обходной путь или изменение кода для получения полной трассировки стека?

Ответы [ 2 ]

4 голосов
/ 14 января 2020

Эта трассировка стека показывает вам, что находится в стеке в то время. Когда вы делаете асинхронный вызов, такой как setTimeout(), он запускает setTimeout(), который регистрирует таймер, который сработает некоторое время в будущем, а затем продолжит выполнение. Так как вы используете await здесь, он приостанавливает выполнение foo(), но продолжает выполнение кода после вызова foo(). Поскольку это также await, он продолжает выполнять код, который называется entrypoint(). После этого стек полностью пуст.

Затем, через некоторое время, ваш таймер срабатывает и его обратный вызов вызывается с полностью чистым стеком. В вашем случае, обратный вызов setTimeout() просто вызывает resolve(), который затем запускает обещание запланировать запуск обработчиков разрешения для следующего события l oop. Это возвращается обратно в систему, и кадр стека снова пуст. На следующем следующем событии события l oop вызываются обработчики разрешения обещаний, которые удовлетворяют await для этого обещания, которое находится внутри контекста функции. Когда это await удовлетворено, остальная часть этой функции начинает выполняться.

Когда эта функция достигает конца своего выполнения, интерпретатор знает, что это был контекст приостановленной функции. В функции нет return, потому что это уже произошло ранее. Вместо этого, поскольку это функция async, конец выполнения функции разрешает обещание, которое возвращает эта функция async. Разрешение этого обещания затем планирует вызов его обработчиков разрешения на следующем тике события l oop, а затем возвращает управление обратно в систему. Кадр стека снова пуст. В этот следующий тик события l oop он вызывает обработчики разрешения, которые удовлетворяют await в операторе await foo(), и функция entrypoint() может продолжать выполняться, определяя, где оно было в последний раз приостановлено.

Итак, ключевым моментом здесь является то, что когда таймер отключается, выполнение переходит с foo обратно на entrypoint, а не через стек и оператор return (эта функция уже вернулась некоторое время go) , но через обещания решаются. Таким образом, в то время, когда таймер отключается и вы затем вызываете crash(), стек действительно пуст, за исключением вызова функции для самого crash().

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

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

К сожалению, единственный известный мне обходной путь - это сделать то, что вы нашли - создать объект Error раньше, когда исходный стек еще жив. Если я правильно помню, здесь обсуждается добавление некоторых функций в язык Javascript, чтобы упростить асинхронную трассировку. Я не помню деталей предложения, но, возможно, вспоминая, какой был стековый фрейм, когда функция была первоначально вызвана, с тех пор, как это было после того, как обещание было разрешено / отклонено и когда объект Error создан, больше не очень полезен.


В случае, если кто-то не был знаком с тем, как работают async функции и как они приостанавливают собственное выполнение после первого await, но потом return рано, вот небольшая демонстрация:

function delay(t) {
    return new Promise(resolve => {
        setTimeout(resolve, t);
    });
}

async function stepA() {
    console.log("5");
    await stepB();
    console.log("6");
}

async function stepB() {
    console.log("3");
    await delay(50);
    console.log("4");
}

console.log("1");
stepA();
console.log("2");

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

1
5
3
2
4
6

Понятно, почему 1 является первым, так как он выполняется первым.

Затем должно быть понятно, почему 5 будет следующим, когда Сначала вызывается stepA().

Затем stepA вызывает stepB(), так как он начинает выполняться, поэтому мы видим 3 next.

Затем stepB вызывает await delay(50). Это выполняет delay(50), который запускает таймер, а затем немедленно возвращает обещание, которое подключено к этому таймеру. Затем он нажимает await и останавливает выполнение stepB.

Когда stepB достигает этого await, в этот момент stepB возвращает обещание, полученное от функции, являющейся async. Это обещание будет привязано к выполнению stepB, которое в конечном итоге (в будущем) получит шанс завершить sh все его выполнение. На данный момент выполнение stepB приостановлено.

Когда stepB возвращает свое обещание, оно возвращается туда, где stepA выполнено await stepB();. Теперь, когда stepB() вернулся (невыполненное обещание), тогда stepA достигает своего await по этому unfulfilled обещанию. Это приостанавливает выполнение stepA и возвращает обещание в этот момент.

Итак, теперь, когда исходный вызов функции stepA() вернулся (невыполненное обещание), и await на этот вызов функции, этот код верхнего уровня после этого вызова функции продолжает выполняться, и мы видим, что консоль выводит 2.

То, что console.log("2") - это последний оператор, который будет выполнен здесь, поэтому управление возвращается обратно переводчик. На этом этапе кадр стека полностью пуст .

Затем, через некоторое время, срабатывает таймер. Это вставляет событие в очередь событий JS. Когда интерпретатор JS свободен, он берет это событие и вызывает обратный вызов таймера, связанный с этим событием. Это делает только одну вещь (вызов resolve() на обещание) и затем возвращается. Вызов решается в тех расписаниях обещаний, которые обещают вызвать обработчики .then() на следующем тике события l oop. Когда это происходит, await в строке кода await delay(50); удовлетворяется, и выполнение этой функции возобновляется. Затем мы видим 4 в консоли, когда выполняется последняя строка stepB.

После выполнения console.log("4"); выполнение stepB завершено, и он может выполнить свое обещание async ( тот, который был им возвращен ранее). Разрешение этого обещания говорит ему о планировании его обработчиков .then() на следующий тик события l oop. Управление возвращается к интерпретатору JS.

На следующем такте события l oop обработчики .then() уведомляют await в await stepB(); о том, что обещание теперь выполнено и выполнение stepA продолжается, и теперь мы см. 6 в консоли. Это последняя строка stepA, которую можно выполнить, чтобы разрешить ее обещание async и вернуть управление обратно в систему.

Как оказалось, никто не слушает обещание async, что вызов stepA() возвращен, поэтому дальнейшее выполнение не выполняется.

2 голосов
/ 06 марта 2020

Я столкнулся с той же проблемой в Узле 12 и обнаружил, что она была исправлена ​​в более поздней версии V8: commit .

Должен ждать Узел 14, я думаю .. .

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

async function crash() {
    await (async () => {throw new Error('dead');})()
        .catch(e => { throw new Error('rethrow'); });
}
...