From 36a7cf9b1e1f96fe27710ab3f06043d01ad54d0e Mon Sep 17 00:00:00 2001 From: Dave Methvin Date: Mon, 23 Nov 2015 13:57:10 -0500 Subject: [PATCH] Deferred: Warn on exceptions that are likely programming errors Fixes gh-2736 Closes gh-2737 --- src/deferred.js | 13 +++++++- src/deferred/exceptionHook.js | 19 +++++++++++ src/jquery.js | 1 + test/unit/deferred.js | 59 +++++++++++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 src/deferred/exceptionHook.js diff --git a/src/deferred.js b/src/deferred.js index ff12ac3e0..6a1ef3b43 100644 --- a/src/deferred.js +++ b/src/deferred.js @@ -157,12 +157,17 @@ jQuery.extend( { mightThrow(); } catch ( e ) { + if ( jQuery.Deferred.exceptionHook ) { + jQuery.Deferred.exceptionHook( e, + process.stackTrace ); + } + // Support: Promises/A+ section 2.3.3.3.4.1 // https://promisesaplus.com/#point-61 // Ignore post-resolution exceptions if ( depth + 1 >= maxDepth ) { - // Only substitue handlers pass on context + // Only substitute handlers pass on context // and multiple values (non-spec behavior) if ( handler !== Thrower ) { that = undefined; @@ -182,6 +187,12 @@ jQuery.extend( { if ( depth ) { process(); } else { + + // Call an optional hook to record the stack, in case of exception + // since it's otherwise lost when execution goes async + if ( jQuery.Deferred.getStackHook ) { + process.stackTrace = jQuery.Deferred.getStackHook(); + } window.setTimeout( process ); } }; diff --git a/src/deferred/exceptionHook.js b/src/deferred/exceptionHook.js new file mode 100644 index 000000000..b9955063a --- /dev/null +++ b/src/deferred/exceptionHook.js @@ -0,0 +1,19 @@ +define( [ + "../core", + "../deferred" +], function( jQuery ) { + +// These usually indicate a programmer mistake during development, +// warn about them ASAP rather than swallowing them by default. +var rerrorNames = /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/; + +jQuery.Deferred.exceptionHook = function( error, stack ) { + + // Support: IE9 + // Console exists when dev tools are open, which can happen at any time + if ( window.console && window.console.warn && error && rerrorNames.test( error.name ) ) { + window.console.warn( "jQuery.Deferred exception: " + error.message, stack ); + } +}; + +} ); diff --git a/src/jquery.js b/src/jquery.js index 2faa9c3cd..4cc9c8a90 100644 --- a/src/jquery.js +++ b/src/jquery.js @@ -4,6 +4,7 @@ define( [ "./traversing", "./callbacks", "./deferred", + "./deferred/exceptionHook", "./core/ready", "./data", "./queue", diff --git a/test/unit/deferred.js b/test/unit/deferred.js index 83c2f4f6b..d65ce34ca 100644 --- a/test/unit/deferred.js +++ b/test/unit/deferred.js @@ -525,6 +525,65 @@ QUnit.test( "jQuery.Deferred.then - spec compatibility", function( assert ) { } catch ( _ ) {} } ); +QUnit[ window.console ? "test" : "skip" ]( "jQuery.Deferred.exceptionHook", function( assert ) { + + assert.expect( 1 ); + + var done = assert.async(), + defer = jQuery.Deferred(), + oldWarn = window.console.warn; + + window.console.warn = function( msg ) { + assert.ok( /barf/.test( msg ), "Message: " + msg ); + }; + jQuery.when( + defer.then( function() { + // Should get an error + jQuery.barf(); + } ).then( null, jQuery.noop ), + defer.then( function() { + // Should NOT get an error + throw new Error( "Make me a sandwich" ); + } ).then( null, jQuery.noop ) + ).then( function( ) { + window.console.warn = oldWarn; + done(); + } ); + + defer.resolve(); +} ); + +QUnit[ window.console ? "test" : "skip" ]( "jQuery.Deferred.exceptionHook with stack hooks", function( assert ) { + + assert.expect( 2 ); + + var done = assert.async(), + defer = jQuery.Deferred(), + oldWarn = window.console.warn; + + jQuery.Deferred.getStackHook = function() { + // Default exceptionHook assumes the stack is in a form console.warn can log, + // but a custom getStackHook+exceptionHook pair could save a raw form and + // format it to a string only when an exception actually occurs. + // For the unit test we just ensure the plumbing works. + return "NO STACK FOR YOU"; + }; + + window.console.warn = function( msg, stack ) { + assert.ok( /cough_up_hairball/.test( msg ), "Function mentioned: " + msg ); + assert.ok( /NO STACK FOR YOU/.test( stack ), "Stack trace included: " + stack ); + }; + defer.then( function() { + jQuery.cough_up_hairball(); + } ).then( null, function( ) { + window.console.warn = oldWarn; + delete jQuery.Deferred.getStackHook; + done(); + } ); + + defer.resolve(); +} ); + QUnit.test( "jQuery.Deferred - 1.x/2.x compatibility", function( assert ) { assert.expect( 8 );