Bug 757282 - Pause when an exception is hit; r=rcampbell
authorPanos Astithas <past@mozilla.com>
Sun, 03 Jun 2012 16:39:51 +0300
changeset 95683 e60ac3f6119d09e208052f7ed77dac3eee3d487d
parent 95682 7e578ad2c6f9373b372dad898fa39334c09c2f56
child 95684 482e07a4fb057702bd521511184bc0637edf7039
push id22827
push userrcampbell@mozilla.com
push dateSun, 03 Jun 2012 20:41:58 +0000
treeherdermozilla-central@0e4f8e1a141b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrcampbell
bugs757282
milestone15.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 757282 - Pause when an exception is hit; r=rcampbell
browser/devtools/debugger/debugger-controller.js
browser/devtools/debugger/debugger-view.js
browser/devtools/debugger/debugger.xul
browser/devtools/debugger/test/Makefile.in
browser/devtools/debugger/test/browser_dbg_pause-exceptions.html
browser/devtools/debugger/test/browser_dbg_pause-exceptions.js
browser/locales/en-US/chrome/browser/devtools/debugger.dtd
toolkit/devtools/debugger/dbg-client.jsm
toolkit/devtools/debugger/server/dbg-script-actors.js
toolkit/devtools/debugger/tests/unit/test_pause_exceptions-01.js
toolkit/devtools/debugger/tests/unit/test_pause_exceptions-02.js
toolkit/devtools/debugger/tests/unit/xpcshell.ini
--- a/browser/devtools/debugger/debugger-controller.js
+++ b/browser/devtools/debugger/debugger-controller.js
@@ -359,37 +359,45 @@ StackFrames.prototype = {
   pageSize: 25,
 
   /**
    * The currently selected frame depth.
    */
   selectedFrame: null,
 
   /**
+   * A flag that defines whether the debuggee will pause whenever an exception
+   * is thrown.
+   */
+  pauseOnExceptions: false,
+
+  /**
    * Gets the current thread the client has connected to.
    */
   get activeThread() {
     return DebuggerController.activeThread;
   },
 
   /**
    * Watch the given thread client.
    *
    * @param function aCallback
    *        The next function in the initialization sequence.
    */
   connect: function SF_connect(aCallback) {
     window.addEventListener("Debugger:FetchedVariables", this._onFetchedVars, false);
 
+    this._onFramesCleared();
+
     this.activeThread.addListener("paused", this._onPaused);
     this.activeThread.addListener("resumed", this._onResume);
     this.activeThread.addListener("framesadded", this._onFrames);
     this.activeThread.addListener("framescleared", this._onFramesCleared);
 
-    this._onFramesCleared();
+    this.updatePauseOnExceptions(this.pauseOnExceptions);
 
     aCallback && aCallback();
   },
 
   /**
    * Disconnect from the client.
    */
   disconnect: function SF_disconnect() {
@@ -401,18 +409,27 @@ StackFrames.prototype = {
     this.activeThread.removeListener("paused", this._onPaused);
     this.activeThread.removeListener("resumed", this._onResume);
     this.activeThread.removeListener("framesadded", this._onFrames);
     this.activeThread.removeListener("framescleared", this._onFramesCleared);
   },
 
   /**
    * Handler for the thread client's paused notification.
+   *
+   * @param string aEvent
+   *        The name of the notification ("paused" in this case).
+   * @param object aPacket
+   *        The response packet.
    */
-  _onPaused: function SF__onPaused() {
+  _onPaused: function SF__onPaused(aEvent, aPacket) {
+    // In case the pause was caused by an exception, store the exception value.
+    if (aPacket.why.type == "exception") {
+      this.exception = aPacket.why.exception;
+    }
     this.activeThread.fillFrames(this.pageSize);
   },
 
   /**
    * Handler for the thread client's resumed notification.
    */
   _onResume: function SF__onResume() {
     DebuggerView.editor.setDebugLocation(-1);
@@ -440,22 +457,23 @@ StackFrames.prototype = {
       DebuggerView.StackFrames.dirty = true;
     }
   },
 
   /**
    * Handler for the thread client's framescleared notification.
    */
   _onFramesCleared: function SF__onFramesCleared() {
+    this.selectedFrame = null;
+    this.exception = null;
     // After each frame step (in, over, out), framescleared is fired, which
     // forces the UI to be emptied and rebuilt on framesadded. Most of the times
     // this is not necessary, and will result in a brief redraw flicker.
     // To avoid it, invalidate the UI only after a short time if necessary.
     window.setTimeout(this._afterFramesCleared, FRAME_STEP_CACHE_DURATION);
-    this.selectedFrame = null;
   },
 
   /**
    * Called soon after the thread client's framescleared notification.
    */
   _afterFramesCleared: function SF__afterFramesCleared() {
     if (!this.activeThread.cachedFrames.length) {
       DebuggerView.StackFrames.emptyText();
@@ -482,16 +500,28 @@ StackFrames.prototype = {
     if (DebuggerView.Scripts.isSelected(url) && line) {
       editor.setDebugLocation(line - 1);
     } else {
       editor.setDebugLocation(-1);
     }
   },
 
   /**
+   * Inform the debugger client whether the debuggee should be paused whenever
+   * an exception is thrown.
+   *
+   * @param boolean aFlag
+   *        The new value of the flag: true for pausing, false otherwise.
+   */
+  updatePauseOnExceptions: function SF_updatePauseOnExceptions(aFlag) {
+    this.pauseOnExceptions = aFlag;
+    this.activeThread.pauseOnExceptions(this.pauseOnExceptions);
+  },
+
+  /**
    * Marks the stack frame in the specified depth as selected and updates the
    * properties view with the stack frame's data.
    *
    * @param number aDepth
    *        The depth of the frame in the stack.
    */
   selectFrame: function SF_selectFrame(aDepth) {
     // Deselect any previously highlighted frame.
@@ -552,24 +582,42 @@ StackFrames.prototype = {
             }
             break;
           default:
             break;
         }
 
         let scope = DebuggerView.Properties.addScope(label);
 
-        // Add "this" to the innermost scope.
-        if (frame.this && env == frame.environment) {
-          let thisVar = scope.addVar("this");
-          thisVar.setGrip({
-            type: frame.this.type,
-            class: frame.this.class
-          });
-          this._addExpander(thisVar, frame.this);
+        // Special additions to the innermost scope.
+        if (env == frame.environment) {
+          // Add any thrown exception.
+          if (aDepth == 0 && this.exception) {
+            let excVar = scope.addVar("<exception>");
+            if (typeof this.exception == "object") {
+              excVar.setGrip({
+                type: this.exception.type,
+                class: this.exception.class
+              });
+              this._addExpander(excVar, this.exception);
+            } else {
+              excVar.setGrip(this.exception);
+            }
+          }
+
+          // Add "this".
+          if (frame.this) {
+            let thisVar = scope.addVar("this");
+            thisVar.setGrip({
+              type: frame.this.type,
+              class: frame.this.class
+            });
+            this._addExpander(thisVar, frame.this);
+          }
+
           // Expand the innermost scope by default.
           scope.expand(true);
           scope.addToHierarchy();
         }
 
         switch (env.type) {
           case "with":
           case "object":
--- a/browser/devtools/debugger/debugger-view.js
+++ b/browser/devtools/debugger/debugger-view.js
@@ -481,16 +481,17 @@ ScriptsView.prototype = {
   }
 };
 
 /**
  * Functions handling the html stackframes UI.
  */
 function StackFramesView() {
   this._onFramesScroll = this._onFramesScroll.bind(this);
+  this._onPauseExceptionsClick = this._onPauseExceptionsClick.bind(this);
   this._onCloseButtonClick = this._onCloseButtonClick.bind(this);
   this._onResumeButtonClick = this._onResumeButtonClick.bind(this);
   this._onStepOverClick = this._onStepOverClick.bind(this);
   this._onStepInClick = this._onStepInClick.bind(this);
   this._onStepOutClick = this._onStepOutClick.bind(this);
 }
 
 StackFramesView.prototype = {
@@ -685,16 +686,24 @@ StackFramesView.prototype = {
   /**
    * Listener handling the close button click event.
    */
   _onCloseButtonClick: function DVF__onCloseButtonClick() {
     DebuggerController.dispatchEvent("Debugger:Close");
   },
 
   /**
+   * Listener handling the pause-on-exceptions click event.
+   */
+  _onPauseExceptionsClick: function DVF__onPauseExceptionsClick() {
+    let option = document.getElementById("pause-exceptions");
+    DebuggerController.StackFrames.updatePauseOnExceptions(option.checked);
+  },
+
+  /**
    * Listener handling the pause/resume button click event.
    */
   _onResumeButtonClick: function DVF__onResumeButtonClick() {
     if (DebuggerController.activeThread.paused) {
       DebuggerController.activeThread.resume();
     } else {
       DebuggerController.activeThread.interrupt();
     }
@@ -731,46 +740,55 @@ StackFramesView.prototype = {
    */
   _frames: null,
 
   /**
    * Initialization function, called when the debugger is initialized.
    */
   initialize: function DVF_initialize() {
     let close = document.getElementById("close");
+    let pauseOnExceptions = document.getElementById("pause-exceptions");
     let resume = document.getElementById("resume");
     let stepOver = document.getElementById("step-over");
     let stepIn = document.getElementById("step-in");
     let stepOut = document.getElementById("step-out");
     let frames = document.getElementById("stackframes");
 
     close.addEventListener("click", this._onCloseButtonClick, false);
+    pauseOnExceptions.checked = DebuggerController.StackFrames.pauseOnExceptions;
+    pauseOnExceptions.addEventListener("click",
+                                        this._onPauseExceptionsClick,
+                                        false);
     resume.addEventListener("click", this._onResumeButtonClick, false);
     stepOver.addEventListener("click", this._onStepOverClick, false);
     stepIn.addEventListener("click", this._onStepInClick, false);
     stepOut.addEventListener("click", this._onStepOutClick, false);
     frames.addEventListener("click", this._onFramesClick, false);
     frames.addEventListener("scroll", this._onFramesScroll, false);
     window.addEventListener("resize", this._onFramesScroll, false);
 
     this._frames = frames;
   },
 
   /**
    * Destruction function, called when the debugger is shut down.
    */
   destroy: function DVF_destroy() {
     let close = document.getElementById("close");
+    let pauseOnExceptions = document.getElementById("pause-exceptions");
     let resume = document.getElementById("resume");
     let stepOver = document.getElementById("step-over");
     let stepIn = document.getElementById("step-in");
     let stepOut = document.getElementById("step-out");
     let frames = this._frames;
 
     close.removeEventListener("click", this._onCloseButtonClick, false);
+    pauseOnExceptions.removeEventListener("click",
+                                          this._onPauseExceptionsClick,
+                                          false);
     resume.removeEventListener("click", this._onResumeButtonClick, false);
     stepOver.removeEventListener("click", this._onStepOverClick, false);
     stepIn.removeEventListener("click", this._onStepInClick, false);
     stepOut.removeEventListener("click", this._onStepOutClick, false);
     frames.removeEventListener("click", this._onFramesClick, false);
     frames.removeEventListener("scroll", this._onFramesScroll, false);
     window.removeEventListener("resize", this._onFramesScroll, false);
 
--- a/browser/devtools/debugger/debugger.xul
+++ b/browser/devtools/debugger/debugger.xul
@@ -63,16 +63,20 @@
                        tooltiptext="&debuggerUI.stepOutButton.tooltip;"
                        tabindex="0"/>
       </hbox>
       <menulist id="scripts" class="devtools-menulist"
                 label="&debuggerUI.emptyScriptText;"/>
       <textbox id="scripts-search" type="search"
                class="devtools-searchinput"
                emptytext="&debuggerUI.emptyFilterText;"/>
+      <checkbox id="pause-exceptions"
+                type="checkbox"
+                tabindex="0"
+                label="&debuggerUI.pauseExceptions;"/>
       <spacer flex="1"/>
 #ifndef XP_MACOSX
       <toolbarbutton id="close"
                      tooltiptext="&debuggerUI.closeButton.tooltip;"
                      class="devtools-closebutton"/>
 #endif
     </toolbar>
     <hbox id="dbg-content" flex="1">
--- a/browser/devtools/debugger/test/Makefile.in
+++ b/browser/devtools/debugger/test/Makefile.in
@@ -49,16 +49,17 @@ include $(topsrcdir)/config/rules.mk
 	browser_dbg_pause-resume.js \
 	browser_dbg_update-editor-mode.js \
 	$(warning browser_dbg_select-line.js temporarily disabled due to oranges, see bug 726609) \
 	browser_dbg_clean-exit.js \
 	browser_dbg_bug723069_editor-breakpoints.js \
 	browser_dbg_bug731394_editor-contextmenu.js \
 	browser_dbg_displayName.js \
 	browser_dbg_iframes.js \
+	browser_dbg_pause-exceptions.js \
 	head.js \
 	$(NULL)
 
 _BROWSER_TEST_PAGES = \
 	browser_dbg_tab1.html \
 	browser_dbg_tab2.html \
 	browser_dbg_debuggerstatement.html \
 	browser_dbg_stack.html \
@@ -66,15 +67,16 @@ include $(topsrcdir)/config/rules.mk
 	test-script-switching-01.js \
 	test-script-switching-02.js \
 	browser_dbg_frame-parameters.html \
 	browser_dbg_update-editor-mode.html \
 	test-editor-mode \
 	browser_dbg_displayName.html \
 	browser_dbg_iframes.html \
 	browser_dbg_with-frame.html \
+	browser_dbg_pause-exceptions.html \
 	$(NULL)
 
 libs:: $(_BROWSER_TEST_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
 
 libs:: $(_BROWSER_TEST_PAGES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_pause-exceptions.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta charset='utf-8'/>
+    <title>Debugger Pause on Exceptions Test</title>
+    <!-- Any copyright is dedicated to the Public Domain.
+         http://creativecommons.org/publicdomain/zero/1.0/ -->
+  </head>
+  <body>
+    <button>Click me!</button>
+    <ul></ul>
+  </body>
+  <script type="text/javascript">
+    window.addEventListener("load", function() {
+      function load() {
+        try {
+          debugger;
+          throw new Error("boom");
+        } catch (e) {
+          var list = document.querySelector("ul");
+          var item = document.createElement("li");
+          item.innerHTML = e.message;
+          list.appendChild(item);
+        }
+      }
+      var button = document.querySelector("button");
+      button.addEventListener("click", load, false);
+    });
+  </script>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/debugger/test/browser_dbg_pause-exceptions.js
@@ -0,0 +1,115 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Make sure that the pause-on-exceptions toggle works.
+ */
+
+const TAB_URL = EXAMPLE_URL + "browser_dbg_pause-exceptions.html";
+
+var gPane = null;
+var gTab = null;
+var gDebugger = null;
+var gCount = 0;
+
+function test()
+{
+  debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
+    gTab = aTab;
+    gPane = aPane;
+    gDebugger = gPane.contentWindow;
+
+    testWithFrame();
+  });
+}
+
+function testWithFrame()
+{
+  gPane.contentWindow.gClient.addOneTimeListener("paused", function() {
+    gDebugger.addEventListener("Debugger:FetchedVariables", function testA() {
+      // We expect 2 Debugger:FetchedVariables events, one from the global object
+      // scope and the regular one.
+      if (++gCount <2) {
+        is(gCount, 1, "A. First Debugger:FetchedVariables event received.");
+        return;
+      }
+      is(gCount, 2, "A. Second Debugger:FetchedVariables event received.");
+      gDebugger.removeEventListener("Debugger:FetchedVariables", testA, false);
+
+      is(gDebugger.DebuggerController.activeThread.state, "paused",
+        "Should be paused now.");
+
+      EventUtils.sendMouseEvent({ type: "click" },
+        gDebugger.document.getElementById("pause-exceptions"),
+        gDebugger);
+
+      is(gDebugger.DebuggerController.StackFrames.pauseOnExceptions, true,
+        "The option should be enabled now.");
+
+      gCount = 0;
+      gPane.contentWindow.gClient.addOneTimeListener("resumed", function() {
+        gDebugger.addEventListener("Debugger:FetchedVariables", function testB() {
+          // We expect 2 Debugger:FetchedVariables events, one from the global object
+          // scope and the regular one.
+          if (++gCount <2) {
+            is(gCount, 1, "B. First Debugger:FetchedVariables event received.");
+            return;
+          }
+          is(gCount, 2, "B. Second Debugger:FetchedVariables event received.");
+          gDebugger.removeEventListener("Debugger:FetchedVariables", testB, false);
+          Services.tm.currentThread.dispatch({ run: function() {
+
+            var frames = gDebugger.DebuggerView.StackFrames._frames,
+                scopes = gDebugger.DebuggerView.Properties._vars,
+                innerScope = scopes.firstChild,
+                innerNodes = innerScope.querySelector(".details").childNodes;
+
+            is(gDebugger.DebuggerController.activeThread.state, "paused",
+              "Should only be getting stack frames while paused.");
+
+            is(frames.querySelectorAll(".dbg-stackframe").length, 1,
+              "Should have one frame.");
+
+            is(scopes.children.length, 3, "Should have 3 variable scopes.");
+
+            is(innerNodes[0].querySelector(".name").getAttribute("value"), "<exception>",
+              "Should have the right property name for the exception.");
+
+            is(innerNodes[0].querySelector(".value").getAttribute("value"), "[object Error]",
+              "Should have the right property value for the exception.");
+
+            resumeAndFinish();
+          }}, 0);
+        }, false);
+      });
+
+      EventUtils.sendMouseEvent({ type: "click" },
+        gDebugger.document.getElementById("resume"),
+        gDebugger);
+    }, false);
+  });
+
+  EventUtils.sendMouseEvent({ type: "click" },
+    content.document.querySelector("button"),
+    content.window);
+}
+
+function resumeAndFinish() {
+  gPane.contentWindow.gClient.addOneTimeListener("resumed", function() {
+    Services.tm.currentThread.dispatch({ run: function() {
+
+      closeDebuggerAndFinish(false);
+    }}, 0);
+  });
+
+  // Resume to let the exception reach it's catch clause.
+  gDebugger.DebuggerController.activeThread.resume();
+}
+
+registerCleanupFunction(function() {
+  removeTab(gTab);
+  gPane = null;
+  gTab = null;
+  gDebugger = null;
+});
--- a/browser/locales/en-US/chrome/browser/devtools/debugger.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/debugger.dtd
@@ -26,16 +26,20 @@
 <!-- LOCALIZATION NOTE (debuggerMenu.commandkey): This is the command key that
   -  launches the debugger UI. Do not translate this one! -->
 <!ENTITY debuggerMenu.commandkey     "S">
 
 <!-- LOCALIZATION NOTE (debuggerUI.closeButton.tooltip): This is the tooltip for
   -  the button that closes the debugger UI. -->
 <!ENTITY debuggerUI.closeButton.tooltip        "Close">
 
+<!-- LOCALIZATION NOTE (debuggerUI.pauseExceptions): This is the label for the
+  -  checkbox that toggles pausing on exceptions. -->
+<!ENTITY debuggerUI.pauseExceptions          "Pause on exceptions">
+
 <!-- LOCALIZATION NOTE (debuggerUI.stepOverButton.tooltip): This is the tooltip for
   -  the button that steps over a function call. -->
 <!ENTITY debuggerUI.stepOverButton.tooltip   "Step Over">
 
 <!-- LOCALIZATION NOTE (debuggerUI.stepInButton): This is the tooltip for the
   -  button that steps into a function call. -->
 <!ENTITY debuggerUI.stepInButton.tooltip     "Step In">
 
--- a/toolkit/devtools/debugger/dbg-client.jsm
+++ b/toolkit/devtools/debugger/dbg-client.jsm
@@ -493,16 +493,18 @@ function ThreadClient(aClient, aActor) {
   this._scriptCache = {};
 }
 
 ThreadClient.prototype = {
   _state: "paused",
   get state() { return this._state; },
   get paused() { return this._state === "paused"; },
 
+  _pauseOnExceptions: false,
+
   _actor: null,
   get actor() { return this._actor; },
 
   _assertPaused: function TC_assertPaused(aCommand) {
     if (!this.paused) {
       throw Error(aCommand + " command sent while not paused.");
     }
   },
@@ -520,18 +522,22 @@ ThreadClient.prototype = {
   resume: function TC_resume(aOnResponse, aLimit) {
     this._assertPaused("resume");
 
     // Put the client in a tentative "resuming" state so we can prevent
     // further requests that should only be sent in the paused state.
     this._state = "resuming";
 
     let self = this;
-    let packet = { to: this._actor, type: DebugProtocolTypes.resume,
-                   resumeLimit: aLimit };
+    let packet = {
+      to: this._actor,
+      type: DebugProtocolTypes.resume,
+      resumeLimit: aLimit,
+      pauseOnExceptions: this._pauseOnExceptions
+    };
     this._client.request(packet, function(aResponse) {
       if (aResponse.error) {
         // There was an error resuming, back to paused state.
         self._state = "paused";
       }
       if (aOnResponse) {
         aOnResponse(aResponse);
       }
@@ -579,16 +585,41 @@ ThreadClient.prototype = {
     this._client.request(packet, function(aResponse) {
       if (aOnResponse) {
         aOnResponse(aResponse);
       }
     });
   },
 
   /**
+   * Enable or disable pausing when an exception is thrown.
+   *
+   * @param boolean aFlag
+   *        Enables pausing if true, disables otherwise.
+   * @param function aOnResponse
+   *        Called with the response packet.
+   */
+  pauseOnExceptions: function TC_pauseOnExceptions(aFlag, aOnResponse) {
+    this._pauseOnExceptions = aFlag;
+    // If the debuggee is paused, the value of the flag will be communicated in
+    // the next resumption. Otherwise we have to force a pause in order to send
+    // the flag.
+    if (!this.paused) {
+      this.interrupt(function(aResponse) {
+        if (aResponse.error) {
+          // Can't continue if pausing failed.
+          aOnResponse(aResponse);
+          return;
+        }
+        this.resume(aOnResponse);
+      }.bind(this));
+    }
+  },
+
+  /**
    * Send a clientEvaluate packet to the debuggee. Response
    * will be a resume packet.
    *
    * @param string aFrame
    *        The actor ID of the frame where the evaluation should take place.
    * @param string aExpression
    *        The expression that will be evaluated in the scope of the frame
    *        above.
--- a/toolkit/devtools/debugger/server/dbg-script-actors.js
+++ b/toolkit/devtools/debugger/server/dbg-script-actors.js
@@ -254,16 +254,20 @@ ThreadActor.prototype = {
             stepFrame.onPop = onPop;
           }
           break;
         default:
           return { error: "badParameterType",
                    message: "Unknown resumeLimit type" };
       }
     }
+
+    if (aRequest && aRequest.pauseOnExceptions) {
+      this.dbg.onExceptionUnwind = this.onExceptionUnwind.bind(this);
+    }
     let packet = this._resumed();
     DebuggerServer.xpcInspector.exitNestedEventLoop();
     return packet;
   },
 
   /**
    * Helper method that returns the next frame when stepping.
    */
@@ -584,16 +588,17 @@ ThreadActor.prototype = {
     // pause-lifetime actors etc) and then repause when complete.
 
     if (this.state === "paused") {
       return undefined;
     }
 
     // Clear stepping hooks.
     this.dbg.onEnterFrame = undefined;
+    this.dbg.onExceptionUnwind = undefined;
     if (aFrame) {
       aFrame.onStep = undefined;
       aFrame.onPop = undefined;
     }
 
     this._state = "paused";
 
     // Save the pause frame (if any) as the youngest frame for
@@ -851,16 +856,43 @@ ThreadActor.prototype = {
    * @param aFrame Debugger.Frame
    *        The stack frame that contained the debugger statement.
    */
   onDebuggerStatement: function TA_onDebuggerStatement(aFrame) {
     return this._pauseAndRespond(aFrame, { type: "debuggerStatement" });
   },
 
   /**
+   * A function that the engine calls when an exception has been thrown and has
+   * propagated to the specified frame.
+   *
+   * @param aFrame Debugger.Frame
+   *        The youngest remaining stack frame.
+   * @param aValue object
+   *        The exception that was thrown.
+   */
+  onExceptionUnwind: function TA_onExceptionUnwind(aFrame, aValue) {
+    try {
+      let packet = this._paused(aFrame);
+      if (!packet) {
+        return undefined;
+      }
+
+      packet.why = { type: "exception",
+                     exception: this.createValueGrip(aValue) };
+      this.conn.send(packet);
+      return this._nest();
+    } catch(e) {
+      Cu.reportError("Got an exception during TA_onExceptionUnwind: " + e +
+                     ": " + e.stack);
+      return undefined;
+    }
+  },
+
+  /**
    * A function that the engine calls when a new script has been loaded into the
    * scope of the specified debuggee global.
    *
    * @param aScript Debugger.Script
    *        The source script that has been loaded into a debuggee compartment.
    * @param aGlobal Debugger.Object
    *        A Debugger.Object instance whose referent is the global object.
    */
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/debugger/tests/unit/test_pause_exceptions-01.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that setting pauseOnExceptions to true will cause the debuggee to pause
+ * when an exceptions is thrown.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+function run_test()
+{
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-stack");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestGlobalClientAndResume(gClient, "test-stack", function(aResponse, aThreadClient) {
+      gThreadClient = aThreadClient;
+      test_pause_frame();
+    });
+  });
+  do_test_pending();
+}
+
+function test_pause_frame()
+{
+  gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
+    gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
+      do_check_eq(aPacket.why.type, "exception");
+      do_check_eq(aPacket.why.exception, 42);
+      gThreadClient.resume(function () {
+        finishClient(gClient);
+      });
+    });
+    gThreadClient.pauseOnExceptions(true);
+    gThreadClient.resume();
+  });
+
+  gDebuggee.eval("(" + function() {
+    function stopMe() {
+      debugger;
+      throw 42;
+    };
+    try {
+      stopMe();
+    } catch (e) {}
+    ")"
+  } + ")()");
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/debugger/tests/unit/test_pause_exceptions-02.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that setting pauseOnExceptions to true when the debugger isn't in a
+ * paused state will cause the debuggee to pause when an exceptions is thrown.
+ */
+
+var gDebuggee;
+var gClient;
+var gThreadClient;
+
+function run_test()
+{
+  initTestDebuggerServer();
+  gDebuggee = addTestGlobal("test-stack");
+  gClient = new DebuggerClient(DebuggerServer.connectPipe());
+  gClient.connect(function() {
+    attachTestGlobalClientAndResume(gClient, "test-stack", function(aResponse, aThreadClient) {
+      gThreadClient = aThreadClient;
+      test_pause_frame();
+    });
+  });
+  do_test_pending();
+}
+
+function test_pause_frame()
+{
+  gThreadClient.pauseOnExceptions(true, function () {
+    gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
+      do_check_eq(aPacket.why.type, "exception");
+      do_check_eq(aPacket.why.exception, 42);
+      gThreadClient.resume(function () {
+        finishClient(gClient);
+      });
+    });
+
+    gDebuggee.eval("(" + function() {
+      function stopMe() {
+        throw 42;
+      };
+      try {
+        stopMe();
+      } catch (e) {}
+      ")"
+    } + ")()");
+  });
+}
--- a/toolkit/devtools/debugger/tests/unit/xpcshell.ini
+++ b/toolkit/devtools/debugger/tests/unit/xpcshell.ini
@@ -53,8 +53,10 @@ tail =
 [test_stepping-02.js]
 [test_stepping-03.js]
 [test_stepping-04.js]
 [test_framebindings-01.js]
 [test_framebindings-02.js]
 [test_framebindings-03.js]
 [test_framebindings-04.js]
 [test_framebindings-05.js]
+[test_pause_exceptions-01.js]
+[test_pause_exceptions-02.js]