From e97affd6038ca0d6fd106db1f240159ce65a8906 Mon Sep 17 00:00:00 2001 From: Mike Kaufman Date: Mon, 18 Jun 2018 16:33:59 -0700 Subject: [PATCH] fixing issue where if a throw occurred in a promise without any attached rejection handlers, we wouldn't notify the debugger that an unhandled exception occurred --- bin/ch/DbgController.js | 2 + .../Language/JavascriptExceptionOperators.cpp | 11 +- .../Language/JavascriptExceptionOperators.h | 2 +- lib/Runtime/Library/JavascriptPromise.cpp | 101 ++++++++++- lib/Runtime/Library/JavascriptPromise.h | 1 + .../JsDiagBreakOnUncaughtException.baseline | 3 + .../JsDiagBreakOnUncaughtException.js | 20 +++ ...agBreakOnUncaughtException.js.dbg.baseline | 18 ++ ...nctions_BreakOnUncaughtExceptions.baseline | 0 ...syncFunctions_BreakOnUncaughtExceptions.js | 35 ++++ ..._BreakOnUncaughtExceptions.js.dbg.baseline | 40 +++++ ...ises_BreakOnFirstChanceExceptions.baseline | 0 ...eakOnFirstChanceExceptions.crosscontext.js | 20 +++ ...InPromises_BreakOnFirstChanceExceptions.js | 97 +++++++++++ ...eakOnFirstChanceExceptions.js.dbg.baseline | 102 +++++++++++ ...romises_BreakOnUncaughtExceptions.baseline | 0 ...onsInPromises_BreakOnUncaughtExceptions.js | 159 ++++++++++++++++++ ..._BreakOnUncaughtExceptions.js.dbg.baseline | 72 ++++++++ test/Debugger/rlexe.xml | 28 +++ 19 files changed, 707 insertions(+), 4 deletions(-) create mode 100644 test/Debugger/JsDiagBreakOnUncaughtException.baseline create mode 100644 test/Debugger/JsDiagBreakOnUncaughtException.js create mode 100644 test/Debugger/JsDiagBreakOnUncaughtException.js.dbg.baseline create mode 100644 test/Debugger/JsDiagExceptionsInAsyncFunctions_BreakOnUncaughtExceptions.baseline create mode 100644 test/Debugger/JsDiagExceptionsInAsyncFunctions_BreakOnUncaughtExceptions.js create mode 100644 test/Debugger/JsDiagExceptionsInAsyncFunctions_BreakOnUncaughtExceptions.js.dbg.baseline create mode 100644 test/Debugger/JsDiagExceptionsInPromises_BreakOnFirstChanceExceptions.baseline create mode 100644 test/Debugger/JsDiagExceptionsInPromises_BreakOnFirstChanceExceptions.crosscontext.js create mode 100644 test/Debugger/JsDiagExceptionsInPromises_BreakOnFirstChanceExceptions.js create mode 100644 test/Debugger/JsDiagExceptionsInPromises_BreakOnFirstChanceExceptions.js.dbg.baseline create mode 100644 test/Debugger/JsDiagExceptionsInPromises_BreakOnUncaughtExceptions.baseline create mode 100644 test/Debugger/JsDiagExceptionsInPromises_BreakOnUncaughtExceptions.js create mode 100644 test/Debugger/JsDiagExceptionsInPromises_BreakOnUncaughtExceptions.js.dbg.baseline diff --git a/bin/ch/DbgController.js b/bin/ch/DbgController.js index b1470e17d6b..9f3744bef8c 100644 --- a/bin/ch/DbgController.js +++ b/bin/ch/DbgController.js @@ -383,6 +383,8 @@ var controllerObj = (function () { if (bpName == "none") { exceptionAttributes = 0; // JsDiagBreakOnExceptionAttributeNone } else if (bpName == "uncaught") { + exceptionAttributes = 0x1; // JsDiagBreakOnExceptionAttributeUncaught + } else if (bpName == "firstchance") { exceptionAttributes = 0x2; // JsDiagBreakOnExceptionAttributeFirstChance } else if (bpName == "all") { exceptionAttributes = 0x1 | 0x2; // JsDiagBreakOnExceptionAttributeUncaught | JsDiagBreakOnExceptionAttributeFirstChance diff --git a/lib/Runtime/Language/JavascriptExceptionOperators.cpp b/lib/Runtime/Language/JavascriptExceptionOperators.cpp index 29a7b7379a7..0550fc160e5 100644 --- a/lib/Runtime/Language/JavascriptExceptionOperators.cpp +++ b/lib/Runtime/Language/JavascriptExceptionOperators.cpp @@ -40,13 +40,22 @@ namespace Js } } - JavascriptExceptionOperators::AutoCatchHandlerExists::AutoCatchHandlerExists(ScriptContext* scriptContext) + JavascriptExceptionOperators::AutoCatchHandlerExists::AutoCatchHandlerExists(ScriptContext* scriptContext, bool isPromiseHandled) { Assert(scriptContext); m_threadContext = scriptContext->GetThreadContext(); Assert(m_threadContext); m_previousCatchHandlerExists = m_threadContext->HasCatchHandler(); m_threadContext->SetHasCatchHandler(TRUE); + + if (!isPromiseHandled) + { + // If this is created from a promise-specific code path, and we don't have a rejection + // handler on the promise, then we want SetCatchHandler to be false so we report any + // unhandled exceptions to any detached debuggers. + m_threadContext->SetHasCatchHandler(false); + } + m_previousCatchHandlerToUserCodeStatus = m_threadContext->IsUserCode(); if (scriptContext->IsScriptContextInDebugMode()) { diff --git a/lib/Runtime/Language/JavascriptExceptionOperators.h b/lib/Runtime/Language/JavascriptExceptionOperators.h index 72d35138bd8..baf3a135ed2 100644 --- a/lib/Runtime/Language/JavascriptExceptionOperators.h +++ b/lib/Runtime/Language/JavascriptExceptionOperators.h @@ -39,7 +39,7 @@ namespace Js void FetchNonUserCodeStatus(ScriptContext *scriptContext); public: - AutoCatchHandlerExists(ScriptContext* scriptContext); + AutoCatchHandlerExists(ScriptContext* scriptContext, bool isPromiseHandled = true); ~AutoCatchHandlerExists(); }; diff --git a/lib/Runtime/Library/JavascriptPromise.cpp b/lib/Runtime/Library/JavascriptPromise.cpp index 49f9724bb66..5180a051ec6 100644 --- a/lib/Runtime/Library/JavascriptPromise.cpp +++ b/lib/Runtime/Library/JavascriptPromise.cpp @@ -932,7 +932,22 @@ namespace Js JavascriptExceptionObject* exception = nullptr; { - Js::JavascriptExceptionOperators::AutoCatchHandlerExists autoCatchHandlerExists(scriptContext); + + bool isPromiseRejectionHandled = true; + if (scriptContext->IsScriptContextInDebugMode()) + { + // only necessary to determine if false if debugger is attached. This way we'll + // correctly break on exceptions raised in promises that result in uhandled rejection + // notifications + Var promiseVar = promiseCapability->GetPromise(); + if (JavascriptPromise::Is(promiseVar)) + { + JavascriptPromise* promise = JavascriptPromise::FromVar(promiseVar); + isPromiseRejectionHandled = !promise->WillRejectionBeUnhandled(); + } + } + + Js::JavascriptExceptionOperators::AutoCatchHandlerExists autoCatchHandlerExists(scriptContext, isPromiseRejectionHandled); try { BEGIN_SAFE_REENTRANT_CALL(scriptContext->GetThreadContext()) @@ -960,6 +975,80 @@ namespace Js return TryCallResolveOrRejectHandler(promiseCapability->GetResolve(), handlerResult, scriptContext); } + + /** + * Determine if at the current point in time, the given promise has a path of reactions that result + * in an unhandled rejection. This doesn't account for potential of a rejection handler added later + * in time. + */ + bool JavascriptPromise::WillRejectionBeUnhandled() + { + bool willBeUnhandled = !this->GetIsHandled(); + if (!willBeUnhandled) + { + // if this promise is handled, then we need to do a depth-first search over this promise's reject + // reactions. If we find a reaction that + // - associated promise is "unhandled" (ie, it's never been "then'd") + // - AND its rejection handler is our default "thrower function" + // then this promise results in an unhandled rejection path. + + JsUtil::Stack stack(&HeapAllocator::Instance); + SimpleHashTable visited(&HeapAllocator::Instance); + stack.Push(this); + visited.Add(this, 1); + + while (!willBeUnhandled && !stack.Empty()) + { + JavascriptPromise * curr = stack.Pop(); + { + JavascriptPromiseReactionList* reactions = curr->GetRejectReactions(); + for (int i = 0; i < reactions->Count(); i++) + { + JavascriptPromiseReaction* reaction = reactions->Item(i); + Var promiseVar = reaction->GetCapabilities()->GetPromise(); + + if (JavascriptPromise::Is(promiseVar)) + { + JavascriptPromise* p = JavascriptPromise::FromVar(promiseVar); + if (!p->GetIsHandled()) + { + RecyclableObject* handler = reaction->GetHandler(); + if (JavascriptFunction::Is(handler)) + { + JavascriptFunction* func = JavascriptFunction::FromVar(handler); + FunctionInfo* functionInfo = func->GetFunctionInfo(); + +#ifdef DEBUG + if (!func->IsCrossSiteObject()) + { + // assert that Thrower function's FunctionInfo hasn't changed + AssertMsg(func->GetScriptContext()->GetLibrary()->GetThrowerFunction()->GetFunctionInfo() == &JavascriptPromise::EntryInfo::Thrower, "unexpected FunctionInfo for thrower function!"); + } +#endif + + // If the function info is the default thrower function's function info, then assume that this is unhandled + // this will work across script contexts + if (functionInfo == &JavascriptPromise::EntryInfo::Thrower) + { + willBeUnhandled = true; + break; + } + } + } + AssertMsg(visited.HasEntry(p) == false, "Unexecpted cycle in promise reaction tree!"); + if (!visited.HasEntry(p)) + { + stack.Push(p); + visited.Add(p, 1); + } + } + } + } + } + } + return willBeUnhandled; + } + Var JavascriptPromise::TryCallResolveOrRejectHandler(Var handler, Var value, ScriptContext* scriptContext) { Var undefinedVar = scriptContext->GetLibrary()->GetUndefined(); @@ -1092,7 +1181,15 @@ namespace Js JavascriptExceptionObject* exception = nullptr; { - Js::JavascriptExceptionOperators::AutoCatchHandlerExists autoCatchHandlerExists(scriptContext); + bool isPromiseRejectionHandled = true; + if (scriptContext->IsScriptContextInDebugMode()) + { + // only necessary to determine if false if debugger is attached. This way we'll + // correctly break on exceptions raised in promises that result in uhandled rejections + isPromiseRejectionHandled = !promise->WillRejectionBeUnhandled(); + } + + Js::JavascriptExceptionOperators::AutoCatchHandlerExists autoCatchHandlerExists(scriptContext, isPromiseRejectionHandled); try { BEGIN_SAFE_REENTRANT_CALL(scriptContext->GetThreadContext()) diff --git a/lib/Runtime/Library/JavascriptPromise.h b/lib/Runtime/Library/JavascriptPromise.h index 1e643d7525f..1aac850c4c8 100644 --- a/lib/Runtime/Library/JavascriptPromise.h +++ b/lib/Runtime/Library/JavascriptPromise.h @@ -563,6 +563,7 @@ namespace Js private : static void AsyncSpawnStep(JavascriptPromiseAsyncSpawnStepArgumentExecutorFunction* nextFunction, JavascriptGenerator* gen, Var resolve, Var reject); + bool WillRejectionBeUnhandled(); #if ENABLE_TTD public: diff --git a/test/Debugger/JsDiagBreakOnUncaughtException.baseline b/test/Debugger/JsDiagBreakOnUncaughtException.baseline new file mode 100644 index 00000000000..56523ee04b5 --- /dev/null +++ b/test/Debugger/JsDiagBreakOnUncaughtException.baseline @@ -0,0 +1,3 @@ +Error: throw exception from throwFunction + at throwFunction() (JsDiagBreakOnUncaughtException.js:18:4) + at Global code (JsDiagBreakOnUncaughtException.js:20:1) diff --git a/test/Debugger/JsDiagBreakOnUncaughtException.js b/test/Debugger/JsDiagBreakOnUncaughtException.js new file mode 100644 index 00000000000..dcc866354f9 --- /dev/null +++ b/test/Debugger/JsDiagBreakOnUncaughtException.js @@ -0,0 +1,20 @@ +//------------------------------------------------------------------------------------------------------- +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. +//------------------------------------------------------------------------------------------------------- + +/**exception(uncaught):stack();**/ + +function noThrowFunction() { + try { + throw new Error("throw exception from noThrowFunction"); + } catch (err) { + } +} +noThrowFunction(); + +// calling throwFunction() will terminate program, so this has to come last +function throwFunction() { + throw new Error("throw exception from throwFunction"); +} +throwFunction(); diff --git a/test/Debugger/JsDiagBreakOnUncaughtException.js.dbg.baseline b/test/Debugger/JsDiagBreakOnUncaughtException.js.dbg.baseline new file mode 100644 index 00000000000..65be5ada3cb --- /dev/null +++ b/test/Debugger/JsDiagBreakOnUncaughtException.js.dbg.baseline @@ -0,0 +1,18 @@ +[ + { + "callStack": [ + { + "line": 17, + "column": 3, + "sourceText": "throw new Error(\"throw exception from throwFunction\")", + "function": "throwFunction" + }, + { + "line": 19, + "column": 0, + "sourceText": "throwFunction()", + "function": "Global code" + } + ] + } +] \ No newline at end of file diff --git a/test/Debugger/JsDiagExceptionsInAsyncFunctions_BreakOnUncaughtExceptions.baseline b/test/Debugger/JsDiagExceptionsInAsyncFunctions_BreakOnUncaughtExceptions.baseline new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/Debugger/JsDiagExceptionsInAsyncFunctions_BreakOnUncaughtExceptions.js b/test/Debugger/JsDiagExceptionsInAsyncFunctions_BreakOnUncaughtExceptions.js new file mode 100644 index 00000000000..05aa09c891b --- /dev/null +++ b/test/Debugger/JsDiagExceptionsInAsyncFunctions_BreakOnUncaughtExceptions.js @@ -0,0 +1,35 @@ +//------------------------------------------------------------------------------------------------------- +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. +//------------------------------------------------------------------------------------------------------- + +/**exception(uncaught):stack();**/ + +async function f1() { + await null; + throw new Error('error in f1'); +} +f1(); + +async function f2() { + + async function f2a() { + throw "err"; + } + + async function f2b() { + try { + var p = f2a(); + } catch (e) { + console.log("caught " + e); + } + } + + async function f2c() { + var p = f2a(); + } + + f2b(); + f2c(); +} +f2(); diff --git a/test/Debugger/JsDiagExceptionsInAsyncFunctions_BreakOnUncaughtExceptions.js.dbg.baseline b/test/Debugger/JsDiagExceptionsInAsyncFunctions_BreakOnUncaughtExceptions.js.dbg.baseline new file mode 100644 index 00000000000..501c9a32117 --- /dev/null +++ b/test/Debugger/JsDiagExceptionsInAsyncFunctions_BreakOnUncaughtExceptions.js.dbg.baseline @@ -0,0 +1,40 @@ +[ + { + "callStack": [ + { + "line": 16, + "column": 8, + "sourceText": "throw \"err\"", + "function": "f2a" + }, + { + "line": 28, + "column": 8, + "sourceText": "var p = f2a()", + "function": "f2c" + }, + { + "line": 32, + "column": 4, + "sourceText": "f2c()", + "function": "f2" + }, + { + "line": 34, + "column": 0, + "sourceText": "f2()", + "function": "Global code" + } + ] + }, + { + "callStack": [ + { + "line": 9, + "column": 4, + "sourceText": "throw new Error('error in f1')", + "function": "f1" + } + ] + } +] \ No newline at end of file diff --git a/test/Debugger/JsDiagExceptionsInPromises_BreakOnFirstChanceExceptions.baseline b/test/Debugger/JsDiagExceptionsInPromises_BreakOnFirstChanceExceptions.baseline new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/Debugger/JsDiagExceptionsInPromises_BreakOnFirstChanceExceptions.crosscontext.js b/test/Debugger/JsDiagExceptionsInPromises_BreakOnFirstChanceExceptions.crosscontext.js new file mode 100644 index 00000000000..38af7ee3d57 --- /dev/null +++ b/test/Debugger/JsDiagExceptionsInPromises_BreakOnFirstChanceExceptions.crosscontext.js @@ -0,0 +1,20 @@ +//------------------------------------------------------------------------------------------------------- +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. +//------------------------------------------------------------------------------------------------------- + +var externalContextPromise = (function () { + let resolvePromise; + let promise = new Promise((resolve, reject) => { + resolvePromise = resolve; + + }); + promise = promise.then(()=> { + throw new Error("error from externalContextPromise1"); + }) + + return { + promise, + resolvePromise + }; +})(); diff --git a/test/Debugger/JsDiagExceptionsInPromises_BreakOnFirstChanceExceptions.js b/test/Debugger/JsDiagExceptionsInPromises_BreakOnFirstChanceExceptions.js new file mode 100644 index 00000000000..3fcbff752e9 --- /dev/null +++ b/test/Debugger/JsDiagExceptionsInPromises_BreakOnFirstChanceExceptions.js @@ -0,0 +1,97 @@ +//------------------------------------------------------------------------------------------------------- +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. +//------------------------------------------------------------------------------------------------------- + +/**exception(firstchance):stack();**/ + + +function unhandledPromiseRejection1() { + Promise.resolve(true) + .then(() => { + throw new Error('error for unhandledPromiseRejection1') + }); +} +unhandledPromiseRejection1(); + +function unhandledPromiseRejection2() { + Promise.resolve(true) + .then(() => { + throw new Error('error for unhandledPromiseRejection2'); + }) + .then(() => { + // no catch + }); +} +unhandledPromiseRejection2(); + +function unhandledPromiseRejection3() { + let p = Promise.resolve(true) + .then(() => { + throw new Error('error for unhandledPromiseRejection3'); + }) + .then(() => 0); + p.then(() => 0).then(() => 1); // this path is not caught + p.then(() => 2, (err) => { }); // this path is caught + +} +unhandledPromiseRejection3(); + +function unhandledPromiseRejection4() { + let p = Promise.resolve(true) + .then(() => { + throw new Error('error for unhandledPromiseRejection3'); + }) + .catch((err) => { + throw err; + }); +} +unhandledPromiseRejection4(); + +function handledPromiseRejection5() { + Promise.resolve(true) + .then(() => { + throw new Error('error for handledPromiseRejection5') + }).catch(() => { }); +} +handledPromiseRejection5(); + +function handledPromiseRejection6() { + Promise.resolve(true) + .then(() => { + throw new Error('error for handledPromiseRejection6'); + }) + .then(() => { }, () => { }); +} +handledPromiseRejection6() + +function handledPromiseRejection7() { + let p = Promise.resolve(true) + .then(() => { + throw new Error('error for handledPromiseRejection7'); + }) + .then(() => 0); + p.then(() => 0).then(() => 1).catch(() => { }); // this path is caught + p.then(() => 2, (err) => { }); // this path is caught + +} +handledPromiseRejection7(); + +function handledPromiseRejection8() { + var p = Promise.resolve(0).then(() => { + p.catch(() => { }); // lazily added catch on the currently executing promise + throw new Error('error for handledPromiseRejection8'); + }); +} +handledPromiseRejection8(); + +function noRejection9() { + let p = Promise.resolve(true) + .then(() => { + try { + throw new Error('error for noRejection9'); + } catch (err) { + } + }); +} +noRejection9(); diff --git a/test/Debugger/JsDiagExceptionsInPromises_BreakOnFirstChanceExceptions.js.dbg.baseline b/test/Debugger/JsDiagExceptionsInPromises_BreakOnFirstChanceExceptions.js.dbg.baseline new file mode 100644 index 00000000000..9331894c933 --- /dev/null +++ b/test/Debugger/JsDiagExceptionsInPromises_BreakOnFirstChanceExceptions.js.dbg.baseline @@ -0,0 +1,102 @@ +[ + { + "callStack": [ + { + "line": 11, + "column": 12, + "sourceText": "throw new Error('error for unhandledPromiseRejection1')", + "function": "Anonymous function" + } + ] + }, + { + "callStack": [ + { + "line": 19, + "column": 12, + "sourceText": "throw new Error('error for unhandledPromiseRejection2')", + "function": "Anonymous function" + } + ] + }, + { + "callStack": [ + { + "line": 30, + "column": 12, + "sourceText": "throw new Error('error for unhandledPromiseRejection3')", + "function": "Anonymous function" + } + ] + }, + { + "callStack": [ + { + "line": 42, + "column": 12, + "sourceText": "throw new Error('error for unhandledPromiseRejection3')", + "function": "Anonymous function" + } + ] + }, + { + "callStack": [ + { + "line": 53, + "column": 12, + "sourceText": "throw new Error('error for handledPromiseRejection5')", + "function": "Anonymous function" + } + ] + }, + { + "callStack": [ + { + "line": 61, + "column": 12, + "sourceText": "throw new Error('error for handledPromiseRejection6')", + "function": "Anonymous function" + } + ] + }, + { + "callStack": [ + { + "line": 70, + "column": 12, + "sourceText": "throw new Error('error for handledPromiseRejection7')", + "function": "Anonymous function" + } + ] + }, + { + "callStack": [ + { + "line": 82, + "column": 8, + "sourceText": "throw new Error('error for handledPromiseRejection8')", + "function": "Anonymous function" + } + ] + }, + { + "callStack": [ + { + "line": 91, + "column": 16, + "sourceText": "throw new Error('error for noRejection9')", + "function": "Anonymous function" + } + ] + }, + { + "callStack": [ + { + "line": 45, + "column": 12, + "sourceText": "throw err", + "function": "Anonymous function" + } + ] + } +] \ No newline at end of file diff --git a/test/Debugger/JsDiagExceptionsInPromises_BreakOnUncaughtExceptions.baseline b/test/Debugger/JsDiagExceptionsInPromises_BreakOnUncaughtExceptions.baseline new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/Debugger/JsDiagExceptionsInPromises_BreakOnUncaughtExceptions.js b/test/Debugger/JsDiagExceptionsInPromises_BreakOnUncaughtExceptions.js new file mode 100644 index 00000000000..a7927baf91e --- /dev/null +++ b/test/Debugger/JsDiagExceptionsInPromises_BreakOnUncaughtExceptions.js @@ -0,0 +1,159 @@ +//------------------------------------------------------------------------------------------------------- +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. +//------------------------------------------------------------------------------------------------------- + +/**exception(uncaught):stack();**/ + +function unhandledPromiseRejection1() { + Promise.resolve(true) + .then(() => { + throw new Error('error for unhandledPromiseRejection1') + }); +} +unhandledPromiseRejection1(); + +function unhandledPromiseRejection2() { + Promise.resolve(true) + .then(() => { + throw new Error('error for unhandledPromiseRejection2'); + }) + .then(() => { + // no catch + }); +} +unhandledPromiseRejection2(); + +function unhandledPromiseRejection3() { + let p = Promise.resolve(true) + .then(() => { + throw new Error('error for unhandledPromiseRejection3'); + }) + .then(() => 0); + p.then(() => 0).then(() => 1); // this path is not caught + p.then(() => 2, (err) => { }); // this path is caught + +} +unhandledPromiseRejection3(); + +function unhandledPromiseRejection4() { + let p = Promise.resolve(true) + .then(() => { + throw new Error('error for unhandledPromiseRejection3'); + }) + .catch((err) => { + throw err; + }); +} +unhandledPromiseRejection4(); + +function handledPromiseRejection5() { + Promise.resolve(true) + .then(() => { + throw new Error('error for handledPromiseRejection5') + }).catch(() => { }); +} +handledPromiseRejection5(); + +function handledPromiseRejection6() { + Promise.resolve(true) + .then(() => { + throw new Error('error for handledPromiseRejection6'); + }) + .then(() => { }, () => { }); +} +handledPromiseRejection6() + +function handledPromiseRejection7() { + let p = Promise.resolve(true) + .then(() => { + throw new Error('error for handledPromiseRejection7'); + }) + .then(() => 0); + p.then(() => 0).then(() => 1).catch(() => { }); // this path is caught + p.then(() => 2, (err) => { }); // this path is caught + +} +handledPromiseRejection7(); + +// +// validate that when we have a handler from one script context +// and a promise from another script context, we'll break appropriately +// +function unhandledPromiseRejectionCrossContext() { + var external = WScript.LoadScriptFile("JsDiagExceptionsInPromises_BreakOnFirstChanceExceptions.crosscontext.js", "samethread"); + let p = Promise.prototype.then.call( + external.externalContextPromise.promise, () => { + }); + external.externalContextPromise.resolvePromise(); +} +unhandledPromiseRejectionCrossContext(); + +// +// validate that when we have a handler from one script context +// and a promise from another script context, we'll not break if a rejection handler is available +// +function handledPromiseRejectionCrossContext() { + var external = WScript.LoadScriptFile("JsDiagExceptionsInPromises_BreakOnFirstChanceExceptions.crosscontext.js", "samethread"); + let p = Promise.prototype.then.call( + external.externalContextPromise.promise, () => {}, () => {}); + external.externalContextPromise.resolvePromise(); +} +handledPromiseRejectionCrossContext(); + +// +// This one below is an edge case where we will break on uncaught exceptions +// even though the rejection is handled. What's happening here is before +// we execute the function onResolve, there is no handler attached +// so we, then as part of executing onResolve, the catch handler is +// attached. We'll break in the function below. +// +// I don't think it is worth fixing this since it seems like a relatively +// rare case. +// +function handledPromiseRejection8_bugbug() { + var p = Promise.resolve(0).then(function onResolve() { + p.catch(() => { }); // lazily added catch on the currently executing promise + throw new Error('error for handledPromiseRejection8_bugbug'); + }); +} +handledPromiseRejection8_bugbug(); + +// +// In the case below, we're resolving a promise with a promise. +// Ultimately, the rejections is handled, but according to +// ES standard, the resolve of promiseA with promiseB gets +// pushed on the task queue, therefore, at the time the exception +// is raised, promiseB hasn't been "then'd". +// +// There are two ways to address this: +// 1. Change the ResolveThenable task to run immediately vs runing in task queue (this would be in violation of the spec) +// 2. Keep a list of the pending resolve-thenable tasks. +// +function handledPromiseRejection9_bugbug() { + function f1() { + let promiseA = new Promise((resolveA, rejectA) => { + let promiseB = Promise.resolve(true).then(() => { + throw new Error('error for handledPromiseRejection9_bugbug'); + }); + resolveA(promiseB); + }); + return promiseA; + } + + f1().catch((e) => { + }); +} +handledPromiseRejection9_bugbug(); + + +function noRejection10() { + let p = Promise.resolve(true) + .then(() => { + try { + throw new Error('error for noRejection10'); + } catch (err) { + } + }); +} +noRejection10(); diff --git a/test/Debugger/JsDiagExceptionsInPromises_BreakOnUncaughtExceptions.js.dbg.baseline b/test/Debugger/JsDiagExceptionsInPromises_BreakOnUncaughtExceptions.js.dbg.baseline new file mode 100644 index 00000000000..ada5542ae98 --- /dev/null +++ b/test/Debugger/JsDiagExceptionsInPromises_BreakOnUncaughtExceptions.js.dbg.baseline @@ -0,0 +1,72 @@ +[ + { + "callStack": [ + { + "line": 10, + "column": 12, + "sourceText": "throw new Error('error for unhandledPromiseRejection1')", + "function": "Anonymous function" + } + ] + }, + { + "callStack": [ + { + "line": 18, + "column": 12, + "sourceText": "throw new Error('error for unhandledPromiseRejection2')", + "function": "Anonymous function" + } + ] + }, + { + "callStack": [ + { + "line": 29, + "column": 12, + "sourceText": "throw new Error('error for unhandledPromiseRejection3')", + "function": "Anonymous function" + } + ] + }, + { + "callStack": [ + { + "line": 7, + "column": 8, + "sourceText": "throw new Error(\"error from externalContextPromise1\")", + "function": "Anonymous function" + } + ] + }, + { + "callStack": [ + { + "line": 116, + "column": 8, + "sourceText": "throw new Error('error for handledPromiseRejection8_bugbug')", + "function": "onResolve" + } + ] + }, + { + "callStack": [ + { + "line": 136, + "column": 16, + "sourceText": "throw new Error('error for handledPromiseRejection9_bugbug')", + "function": "Anonymous function" + } + ] + }, + { + "callStack": [ + { + "line": 44, + "column": 12, + "sourceText": "throw err", + "function": "Anonymous function" + } + ] + } +] \ No newline at end of file diff --git a/test/Debugger/rlexe.xml b/test/Debugger/rlexe.xml index 5168190251a..5b8f794f031 100644 --- a/test/Debugger/rlexe.xml +++ b/test/Debugger/rlexe.xml @@ -18,6 +18,34 @@ JsDiagBreakpoints_ArrayBuffer.js + + + -debuglaunch -dbgbaseline:JsDiagBreakOnUncaughtException.js.dbg.baseline + JsDiagBreakOnUncaughtException.baseline + JsDiagBreakOnUncaughtException.js + + + + + -debuglaunch -dbgbaseline:JsDiagExceptionsInPromises_BreakOnUncaughtExceptions.js.dbg.baseline + JsDiagExceptionsInPromises_BreakOnUncaughtExceptions.baseline + JsDiagExceptionsInPromises_BreakOnUncaughtExceptions.js + + + + + -debuglaunch -dbgbaseline:JsDiagExceptionsInPromises_BreakOnFirstChanceExceptions.js.dbg.baseline + JsDiagExceptionsInPromises_BreakOnFirstChanceExceptions.baseline + JsDiagExceptionsInPromises_BreakOnFirstChanceExceptions.js + + + + + -debuglaunch -dbgbaseline:JsDiagExceptionsInAsyncFunctions_BreakOnUncaughtExceptions.js.dbg.baseline + JsDiagExceptionsInAsyncFunctions_BreakOnUncaughtExceptions.baseline + JsDiagExceptionsInAsyncFunctions_BreakOnUncaughtExceptions.js + + -debuglaunch -dbgbaseline:JsDiagEvaluate.js.dbg.baseline