diff --git a/bin/ChakraCore/ChakraCore.def b/bin/ChakraCore/ChakraCore.def
index ca2a01dd616..a996d102fb4 100644
--- a/bin/ChakraCore/ChakraCore.def
+++ b/bin/ChakraCore/ChakraCore.def
@@ -60,3 +60,5 @@ JsLessThan
JsLessThanOrEqual
JsCreateEnhancedFunction
+
+JsSetHostPromiseRejectionTracker
\ No newline at end of file
diff --git a/bin/ch/ChakraRtInterface.cpp b/bin/ch/ChakraRtInterface.cpp
index 191fa1c499f..503d6b8c025 100644
--- a/bin/ch/ChakraRtInterface.cpp
+++ b/bin/ch/ChakraRtInterface.cpp
@@ -120,6 +120,7 @@ bool ChakraRTInterface::LoadChakraDll(ArgInfo* argInfo, HINSTANCE *outLibrary)
m_jsApiHooks.pfJsrtGetValueType = (JsAPIHooks::JsrtGetValueType)GetChakraCoreSymbol(library, "JsGetValueType");
m_jsApiHooks.pfJsrtSetIndexedProperty = (JsAPIHooks::JsrtSetIndexedPropertyPtr)GetChakraCoreSymbol(library, "JsSetIndexedProperty");
m_jsApiHooks.pfJsrtSetPromiseContinuationCallback = (JsAPIHooks::JsrtSetPromiseContinuationCallbackPtr)GetChakraCoreSymbol(library, "JsSetPromiseContinuationCallback");
+ m_jsApiHooks.pfJsrtSetHostPromiseRejectionTracker = (JsAPIHooks::JsrtSetHostPromiseRejectionTrackerPtr)GetChakraCoreSymbol(library, "JsSetHostPromiseRejectionTracker");
m_jsApiHooks.pfJsrtGetContextOfObject = (JsAPIHooks::JsrtGetContextOfObject)GetChakraCoreSymbol(library, "JsGetContextOfObject");
m_jsApiHooks.pfJsrtInitializeModuleRecord = (JsAPIHooks::JsInitializeModuleRecordPtr)GetChakraCoreSymbol(library, "JsInitializeModuleRecord");
m_jsApiHooks.pfJsrtParseModuleSource = (JsAPIHooks::JsParseModuleSourcePtr)GetChakraCoreSymbol(library, "JsParseModuleSource");
diff --git a/bin/ch/ChakraRtInterface.h b/bin/ch/ChakraRtInterface.h
index 6dbc3f77bd2..c9693f288b5 100644
--- a/bin/ch/ChakraRtInterface.h
+++ b/bin/ch/ChakraRtInterface.h
@@ -54,6 +54,7 @@ struct JsAPIHooks
typedef JsErrorCode (WINAPI *JsrtGetValueType)(JsValueRef value, JsValueType *type);
typedef JsErrorCode (WINAPI *JsrtSetIndexedPropertyPtr)(JsValueRef object, JsValueRef index, JsValueRef value);
typedef JsErrorCode (WINAPI *JsrtSetPromiseContinuationCallbackPtr)(JsPromiseContinuationCallback callback, void *callbackState);
+ typedef JsErrorCode (WINAPI *JsrtSetHostPromiseRejectionTrackerPtr)(JsHostPromiseRejectionTrackerCallback callback);
typedef JsErrorCode (WINAPI *JsrtGetContextOfObject)(JsValueRef object, JsContextRef *callbackState);
typedef JsErrorCode(WINAPI *JsrtDiagStartDebugging)(JsRuntimeHandle runtimeHandle, JsDiagDebugEventCallback debugEventCallback, void* callbackState);
@@ -152,6 +153,7 @@ struct JsAPIHooks
JsrtGetValueType pfJsrtGetValueType;
JsrtSetIndexedPropertyPtr pfJsrtSetIndexedProperty;
JsrtSetPromiseContinuationCallbackPtr pfJsrtSetPromiseContinuationCallback;
+ JsrtSetHostPromiseRejectionTrackerPtr pfJsrtSetHostPromiseRejectionTracker;
JsrtGetContextOfObject pfJsrtGetContextOfObject;
JsrtDiagStartDebugging pfJsrtDiagStartDebugging;
JsrtDiagStopDebugging pfJsrtDiagStopDebugging;
@@ -356,6 +358,7 @@ class ChakraRTInterface
static JsErrorCode WINAPI JsGetValueType(JsValueRef value, JsValueType *type) { return HOOK_JS_API(GetValueType(value, type)); }
static JsErrorCode WINAPI JsSetIndexedProperty(JsValueRef object, JsValueRef index, JsValueRef value) { return HOOK_JS_API(SetIndexedProperty(object, index, value)); }
static JsErrorCode WINAPI JsSetPromiseContinuationCallback(JsPromiseContinuationCallback callback, void *callbackState) { return HOOK_JS_API(SetPromiseContinuationCallback(callback, callbackState)); }
+ static JsErrorCode WINAPI JsSetHostPromiseRejectionTracker(JsHostPromiseRejectionTrackerCallback callback) { return HOOK_JS_API(SetHostPromiseRejectionTracker(callback)); }
static JsErrorCode WINAPI JsGetContextOfObject(JsValueRef object, JsContextRef* context) { return HOOK_JS_API(GetContextOfObject(object, context)); }
static JsErrorCode WINAPI JsDiagStartDebugging(JsRuntimeHandle runtimeHandle, JsDiagDebugEventCallback debugEventCallback, void* callbackState) { return HOOK_JS_API(DiagStartDebugging(runtimeHandle, debugEventCallback, callbackState)); }
static JsErrorCode WINAPI JsDiagStopDebugging(JsRuntimeHandle runtimeHandle, void** callbackState) { return HOOK_JS_API(DiagStopDebugging(runtimeHandle, callbackState)); }
diff --git a/bin/ch/HostConfigFlagsList.h b/bin/ch/HostConfigFlagsList.h
index a2fa5bd75b1..0749efa34ec 100644
--- a/bin/ch/HostConfigFlagsList.h
+++ b/bin/ch/HostConfigFlagsList.h
@@ -15,5 +15,6 @@ FLAG(bool, IgnoreScriptErrorCode, "Don't return error code on script e
FLAG(bool, MuteHostErrorMsg, "Mute host error output, e.g. module load failures", false)
FLAG(bool, TraceHostCallback, "Output traces for host callbacks", false)
FLAG(bool, Test262, "load Test262 harness", false)
+FLAG(bool, TrackRejectedPromises, "Enable tracking of unhandled promise rejections", false)
#undef FLAG
#endif
diff --git a/bin/ch/WScriptJsrt.cpp b/bin/ch/WScriptJsrt.cpp
index 485ed60d73c..f9b412d1734 100644
--- a/bin/ch/WScriptJsrt.cpp
+++ b/bin/ch/WScriptJsrt.cpp
@@ -1871,3 +1871,19 @@ void WScriptJsrt::PromiseContinuationCallback(JsValueRef task, void *callbackSta
WScriptJsrt::CallbackMessage *msg = new WScriptJsrt::CallbackMessage(0, task);
messageQueue->InsertSorted(msg);
}
+
+void WScriptJsrt::PromiseRejectionTrackerCallback(JsValueRef promise, JsValueRef reason, bool handled)
+{
+ Assert(promise != JS_INVALID_REFERENCE);
+ Assert(reason != JS_INVALID_REFERENCE);
+ if(!handled)
+ {
+ wprintf(_u("Uncaught promise rejection\n"));
+ }
+ else
+ {
+ wprintf(_u("Promise rejection handled\n"));
+ }
+ fflush(stdout);
+}
+
diff --git a/bin/ch/WScriptJsrt.h b/bin/ch/WScriptJsrt.h
index 3ac3be5452f..4d408d9d11f 100644
--- a/bin/ch/WScriptJsrt.h
+++ b/bin/ch/WScriptJsrt.h
@@ -58,6 +58,7 @@ class WScriptJsrt
static JsErrorCode NotifyModuleReadyCallback(_In_opt_ JsModuleRecord referencingModule, _In_opt_ JsValueRef exceptionVar);
static JsErrorCode InitializeModuleCallbacks();
static void CALLBACK PromiseContinuationCallback(JsValueRef task, void *callbackState);
+ static void CALLBACK PromiseRejectionTrackerCallback(JsValueRef promise, JsValueRef reason, bool handled);
static LPCWSTR ConvertErrorCodeToMessage(JsErrorCode errorCode)
{
diff --git a/bin/ch/ch.cpp b/bin/ch/ch.cpp
index fbd39ecfe47..8f0aea23d30 100644
--- a/bin/ch/ch.cpp
+++ b/bin/ch/ch.cpp
@@ -757,6 +757,10 @@ HRESULT ExecuteTest(const char* fileName)
IfFailGo(E_FAIL);
}
+ if(HostConfigFlags::flags.TrackRejectedPromises)
+ {
+ ChakraRTInterface::JsSetHostPromiseRejectionTracker(WScriptJsrt::PromiseRejectionTrackerCallback);
+ }
len = strlen(fullPath);
if (HostConfigFlags::flags.GenerateLibraryByteCodeHeaderIsEnabled)
{
diff --git a/lib/Jsrt/ChakraCore.h b/lib/Jsrt/ChakraCore.h
index c0c7dd2a1cb..b263bf81f8b 100644
--- a/lib/Jsrt/ChakraCore.h
+++ b/lib/Jsrt/ChakraCore.h
@@ -130,6 +130,21 @@ typedef struct JsNativeFunctionInfo
/// The result of the call, if any.
typedef _Ret_maybenull_ JsValueRef(CHAKRA_CALLBACK * JsEnhancedNativeFunction)(_In_ JsValueRef callee, _In_ JsValueRef *arguments, _In_ unsigned short argumentCount, _In_ JsNativeFunctionInfo *info, _In_opt_ void *callbackState);
+///
+/// A Promise Rejection Tracker callback.
+///
+///
+/// The host can specify a promise rejection tracker callback in JsSetHostPromiseRejetionTracker.
+/// If a promise is rejected with no reactions or a reaction is added to a promise that was rejected
+/// before it had reactions by default nothing is done.
+/// A Promise Rejection Tracker callback may be set - which will then be called when this occurs.
+///
+/// The promise object, represented as a JsValueRef.
+/// The value/cause of the rejection, represented as a JsValueRef.
+/// Boolean - false for promiseRejected: i.e. if the promise has just been rejected with no handler,
+/// true for promiseHandled: i.e. if it was rejected before without a handler and is now being handled.
+typedef void (CHAKRA_CALLBACK *JsHostPromiseRejectionTrackerCallback)(_In_ JsValueRef promise, _In_ JsValueRef reason, _In_ bool handled);
+
///
/// Creates a new enhanced JavaScript function.
///
@@ -993,5 +1008,26 @@ CHAKRA_API
_In_ JsValueRef object,
_In_ JsValueRef key,
_Out_ bool *hasOwnProperty);
+
+
+///
+/// Sets whether any action should be taken when a promise is rejected with no reactions
+/// or a reaction is added to a promise that was rejected before it had reactions.
+/// By default in either of these cases nothing occurs.
+/// This function allows you to specify if something should occur and provide a callback
+/// to implement whatever should occur.
+///
+///
+///
+/// Requires an active script context.
+///
+///
+/// The callback function being set.
+///
+/// The code JsNoError if the operation succeeded, a failure code otherwise.
+///
+CHAKRA_API
+ JsSetHostPromiseRejectionTracker(
+ _In_ JsHostPromiseRejectionTrackerCallback promiseRejectionTrackerCallback);
#endif // _CHAKRACOREBUILD
#endif // _CHAKRACORE_H_
diff --git a/lib/Jsrt/Jsrt.cpp b/lib/Jsrt/Jsrt.cpp
index 6aed238865c..9f8a813d871 100644
--- a/lib/Jsrt/Jsrt.cpp
+++ b/lib/Jsrt/Jsrt.cpp
@@ -3457,6 +3457,17 @@ CHAKRA_API JsSetPromiseContinuationCallback(_In_opt_ JsPromiseContinuationCallba
/*allowInObjectBeforeCollectCallback*/true);
}
+CHAKRA_API JsSetHostPromiseRejectionTracker(_In_ JsHostPromiseRejectionTrackerCallback promiseRejectionTrackerCallback)
+{
+ return ContextAPINoScriptWrapper_NoRecord([&](Js::ScriptContext *scriptContext) -> JsErrorCode {
+ scriptContext->GetLibrary()->SetNativeHostPromiseRejectionTrackerCallback((Js::JavascriptLibrary::HostPromiseRejectionTrackerCallback) promiseRejectionTrackerCallback);
+ return JsNoError;
+ },
+ /*allowInObjectBeforeCollectCallback*/true);
+}
+
+
+
JsErrorCode RunScriptCore(JsValueRef scriptSource, const byte *script, size_t cb,
LoadScriptFlag loadScriptFlag, JsSourceContext sourceContext,
const WCHAR *sourceUrl, bool parseOnly, JsParseScriptAttributes parseAttributes,
diff --git a/lib/Runtime/Library/JavascriptLibrary.cpp b/lib/Runtime/Library/JavascriptLibrary.cpp
index e7b91ff2aff..9e1728dace1 100644
--- a/lib/Runtime/Library/JavascriptLibrary.cpp
+++ b/lib/Runtime/Library/JavascriptLibrary.cpp
@@ -5295,6 +5295,32 @@ namespace Js
this->nativeHostPromiseContinuationFunctionState = state;
}
+ void JavascriptLibrary::SetNativeHostPromiseRejectionTrackerCallback(HostPromiseRejectionTrackerCallback function)
+ {
+ this->nativeHostPromiseRejectionTracker = function;
+ this->hostShouldTrackPromiseRejections = true;
+ }
+
+ void JavascriptLibrary::CallNativeHostPromiseRejectionTracker(Var promise, Var reason, bool handled)
+ {
+ if(hostShouldTrackPromiseRejections)
+ {
+ BEGIN_LEAVE_SCRIPT(scriptContext);
+ try
+ {
+ nativeHostPromiseRejectionTracker(promise, reason, handled);
+ }
+ catch (...)
+ {
+ // Hosts are required not to pass exceptions back across the callback boundary. If
+ // this happens, it is a bug in the host, not something that we are expected to
+ // handle gracefully.
+ Js::Throw::FatalInternalError();
+ }
+ END_LEAVE_SCRIPT(scriptContext);
+ }
+ }
+
void JavascriptLibrary::SetJsrtContext(FinalizableObject* jsrtContext)
{
// With JsrtContext supporting cross context, ensure that it doesn't get GCed
diff --git a/lib/Runtime/Library/JavascriptLibrary.h b/lib/Runtime/Library/JavascriptLibrary.h
index 6d94aedaee6..53dc0806cb3 100644
--- a/lib/Runtime/Library/JavascriptLibrary.h
+++ b/lib/Runtime/Library/JavascriptLibrary.h
@@ -242,6 +242,7 @@ namespace Js
static DWORD GetRandSeed1Offset() { return offsetof(JavascriptLibrary, randSeed1); }
static DWORD GetTypeDisplayStringsOffset() { return offsetof(JavascriptLibrary, typeDisplayStrings); }
typedef bool (CALLBACK *PromiseContinuationCallback)(Var task, void *callbackState);
+ typedef void (CALLBACK *HostPromiseRejectionTrackerCallback)(Var promise, Var reason, bool handled);
Var GetUndeclBlockVar() const { return undeclBlockVarSentinel; }
bool IsUndeclBlockVar(Var var) const { return var == undeclBlockVarSentinel; }
@@ -492,6 +493,9 @@ namespace Js
FieldNoBarrier(PromiseContinuationCallback) nativeHostPromiseContinuationFunction;
Field(void *) nativeHostPromiseContinuationFunctionState;
+ FieldNoBarrier(HostPromiseRejectionTrackerCallback) nativeHostPromiseRejectionTracker;
+ FieldNoBarrier(bool) hostShouldTrackPromiseRejections = false;
+
typedef SList FunctionReferenceList;
typedef JsUtil::WeakReferenceDictionary> JsrtExternalTypesCache;
@@ -949,6 +953,9 @@ namespace Js
JavascriptFunction* GetThrowerFunction() const { return throwerFunction; }
void SetNativeHostPromiseContinuationFunction(PromiseContinuationCallback function, void *state);
+ void SetNativeHostPromiseRejectionTrackerCallback(HostPromiseRejectionTrackerCallback function);
+ void CallNativeHostPromiseRejectionTracker(Var promise, Var reason, bool handled);
+
void SetJsrtContext(FinalizableObject* jsrtContext);
FinalizableObject* GetJsrtContext();
diff --git a/lib/Runtime/Library/JavascriptPromise.cpp b/lib/Runtime/Library/JavascriptPromise.cpp
index 27a33e62bcf..07a1424d2c8 100644
--- a/lib/Runtime/Library/JavascriptPromise.cpp
+++ b/lib/Runtime/Library/JavascriptPromise.cpp
@@ -12,6 +12,7 @@ namespace Js
Assert(type->GetTypeId() == TypeIds_Promise);
this->status = PromiseStatusCode_Undefined;
+ this->isHandled = false;
this->result = nullptr;
this->resolveReactions = nullptr;
this->rejectReactions = nullptr;
@@ -660,6 +661,10 @@ namespace Js
{
reactions = this->GetRejectReactions();
newStatus = PromiseStatusCode_HasRejection;
+ if(!GetIsHandled())
+ {
+ scriptContext->GetLibrary()->CallNativeHostPromiseRejectionTracker(this, resolution, false);
+ }
}
else
{
@@ -838,6 +843,10 @@ namespace Js
EnqueuePromiseReactionTask(resolveReaction, sourcePromise->result, scriptContext);
break;
case PromiseStatusCode_HasRejection:
+ if(!sourcePromise->GetIsHandled())
+ {
+ scriptContext->GetLibrary()->CallNativeHostPromiseRejectionTracker(sourcePromise, sourcePromise->result, true);
+ }
EnqueuePromiseReactionTask(rejectReaction, sourcePromise->result, scriptContext);
break;
default:
@@ -845,6 +854,8 @@ namespace Js
break;
}
+ sourcePromise->SetIsHandled();
+
return promiseCapability->GetPromise();
}
diff --git a/lib/Runtime/Library/JavascriptPromise.h b/lib/Runtime/Library/JavascriptPromise.h
index ae3b6791eb8..6d78aa81889 100644
--- a/lib/Runtime/Library/JavascriptPromise.h
+++ b/lib/Runtime/Library/JavascriptPromise.h
@@ -461,6 +461,8 @@ namespace Js
PromiseStatusCode_HasRejection
};
+ bool GetIsHandled() {return isHandled;}
+ void SetIsHandled() {isHandled = true;}
PromiseStatus GetStatus() const { return status; }
Var GetResult() const { return result; }
@@ -470,6 +472,7 @@ namespace Js
protected:
Field(PromiseStatus) status;
Field(Var) result;
+ Field(bool) isHandled;
Field(JavascriptPromiseReactionList*) resolveReactions;
Field(JavascriptPromiseReactionList*) rejectReactions;
diff --git a/test/es7/PromiseRejectionTracking.baseline b/test/es7/PromiseRejectionTracking.baseline
new file mode 100644
index 00000000000..059778f2510
--- /dev/null
+++ b/test/es7/PromiseRejectionTracking.baseline
@@ -0,0 +1,19 @@
+Executing test #1 - Reject promise with no reactions.
+Uncaught promise rejection
+Executing test #2 - Reject promise with a catch reaction only.
+Executing test #3 - Reject promise with catch and then reactions.
+Executing test #4 - Reject promise then add a catch afterwards.
+Uncaught promise rejection
+Promise rejection handled
+Executing test #5 - Reject promise then add two catches afterwards.
+Uncaught promise rejection
+Promise rejection handled
+Executing test #6 - Async function that throws.
+Uncaught promise rejection
+Executing test #7 - Async function that throws but is caught.
+Uncaught promise rejection
+Promise rejection handled
+Executing test #8 - Async function that awaits a function that throws.
+Uncaught promise rejection
+Promise rejection handled
+Uncaught promise rejection
diff --git a/test/es7/PromiseRejectionTracking.js b/test/es7/PromiseRejectionTracking.js
new file mode 100644
index 00000000000..886c0c283d7
--- /dev/null
+++ b/test/es7/PromiseRejectionTracking.js
@@ -0,0 +1,110 @@
+//-------------------------------------------------------------------------------------------------------
+// Copyright (C) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
+//-------------------------------------------------------------------------------------------------------
+
+// Test HostPromiseRejectionTracker - see ecma262 section 25.4.1.9
+
+let tests = [
+ {
+ name: "Reject promise with no reactions.",
+ body: function(index)
+ {
+ let controller;
+ let promise = new Promise((resolve, reject)=>{
+ controller = {resolve, reject};
+ });
+ controller.reject();//Should notify rejected
+ }
+ },
+ {
+ name: "Reject promise with a catch reaction only.",
+ body: function(index)
+ {
+ let controller;
+ let promise = new Promise((resolve, reject)=>{
+ controller = {resolve, reject};
+ }).catch(()=>{});
+ controller.reject();//Should NOT notify
+ }
+ },
+ {
+ name: "Reject promise with catch and then reactions.",
+ body: function(index)
+ {
+ let controller;
+ let promise = new Promise((resolve, reject)=>{
+ controller = {resolve, reject};
+ }).then(()=>{}).catch(()=>{});
+ controller.reject();//Should NOT notify
+ }
+ },
+ {
+ name: "Reject promise then add a catch afterwards.",
+ body: function(index)
+ {
+ let controller;
+ let promise = new Promise((resolve, reject)=>{
+ controller = {resolve, reject};
+ });
+ controller.reject();//Should notify rejected
+ promise.catch(()=>{});//Should notify handled
+ }
+ },
+ {
+ name: "Reject promise then add two catches afterwards.",
+ body: function(index)
+ {
+ let controller;
+ let promise = new Promise((resolve, reject)=>{
+ controller = {resolve, reject};
+ });
+ controller.reject();//Should notify rejected
+ promise.catch(()=>{});//Should notify handled
+ promise.catch(()=>{});//Should NOT notify
+ }
+ },
+ {
+ name: "Async function that throws.",
+ body: function(index)
+ {
+ async function aFunction()
+ {
+ throw "throwing";
+ }
+ aFunction();//Should notify rejected
+ }
+ },
+ {
+ name: "Async function that throws but is caught.",
+ body: function(index)
+ {
+ async function aFunction()
+ {
+ throw "throwing";
+ }
+ aFunction().catch(()=>{});//Should notify rejected AND then handled
+ }
+ },
+ {
+ name: "Async function that awaits a function that throws.",
+ body: function(index)
+ {
+ async function aFunction()
+ {
+ throw "throwing";//Should notify rejected
+ }
+ async function bFunction()
+ {
+ await aFunction();//Should notify handled
+ }
+ bFunction();//Should notify rejected
+ }
+ }
+];
+
+for(let i = 0; i < tests.length; ++i)
+{
+ WScript.Echo('Executing test #' + (i + 1) + ' - ' + tests[i].name);
+ tests[i].body(i+1);
+}
diff --git a/test/es7/rlexe.xml b/test/es7/rlexe.xml
index 9a23c30fd1c..8c34409e4c2 100644
--- a/test/es7/rlexe.xml
+++ b/test/es7/rlexe.xml
@@ -92,4 +92,11 @@
exclude_xplat
+
+
+ PromiseRejectionTracking.js
+ -TrackRejectedPromises -args summary -endargs -nodeferparse
+ PromiseRejectionTracking.baseline
+
+