Bug 1336214 - Preserve stacktraces in Marionette errors. r=maja_zf, a=test-only
authorAndreas Tolfsen <ato@mozilla.com>
Fri, 10 Feb 2017 18:36:52 +0000
changeset 378790 fd1a80b2522dae0c0aa61d0e7338ea79f9a9ac9a
parent 378789 500e3282b566bfda4db95ab2526c10f8b2bdcff4
child 378791 514641af5ac48af57a8d2afef1141a06928be9b5
push id1419
push userjlund@mozilla.com
push dateMon, 10 Apr 2017 20:44:07 +0000
treeherdermozilla-release@5e6801b73ef6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmaja_zf, test-only
bugs1336214, 1326534
milestone53.0a2
Bug 1336214 - Preserve stacktraces in Marionette errors. r=maja_zf, a=test-only Since bug 1326534 we have discarded the original stacktrace from errors originating inside Marionette. This was due to faulty logic when attempting to generate a new stacktrace when it was missing from a propagated error. This change simplifies WebDriver errors by making use of Error inheritance. The WebDriver error specific functions error.toJson and error.fromJson has additionally been moved to WebDriverError. MozReview-Commit-ID: C3Ns0H01LyG
testing/marionette/assert.js
testing/marionette/error.js
testing/marionette/message.js
testing/marionette/proxy.js
testing/marionette/test_assert.js
testing/marionette/test_error.js
testing/marionette/test_message.js
--- a/testing/marionette/assert.js
+++ b/testing/marionette/assert.js
@@ -27,22 +27,22 @@ this.assert = {};
  * @param {GeckoDriver} driver
  *     Marionette driver instance.
  * @param {string=} msg
  *     Custom error message.
  *
  * @return {string}
  *     Session ID.
  *
- * @throws {InvalidSessionIdError}
+ * @throws {InvalidSessionIDError}
  *     If |driver| does not have a session ID.
  */
 assert.session = function (driver, msg = "") {
   assert.that(sessionID => sessionID,
-      msg, InvalidSessionIdError)(driver.sessionId);
+      msg, InvalidSessionIDError)(driver.sessionId);
   return driver.sessionId;
 };
 
 /**
  * Asserts that the current browser is Firefox Desktop.
  *
  * @param {string=} msg
  *     Custom error message.
--- a/testing/marionette/error.js
+++ b/testing/marionette/error.js
@@ -8,17 +8,17 @@ const {interfaces: Ci, utils: Cu} = Comp
 
 const ERRORS = new Set([
   "ElementNotAccessibleError",
   "ElementNotVisibleError",
   "InsecureCertificateError",
   "InvalidArgumentError",
   "InvalidElementStateError",
   "InvalidSelectorError",
-  "InvalidSessionIdError",
+  "InvalidSessionIDError",
   "JavaScriptError",
   "NoAlertOpenError",
   "NoSuchElementError",
   "NoSuchFrameError",
   "NoSuchWindowError",
   "ScriptTimeoutError",
   "SessionNotCreatedError",
   "StaleElementReferenceError",
@@ -76,32 +76,33 @@ error.isError = function (val) {
  * Checks if obj is an object in the WebDriverError prototypal chain.
  */
 error.isWebDriverError = function (obj) {
   return error.isError(obj) &&
       ("name" in obj && ERRORS.has(obj.name));
 };
 
 /**
- * Wraps an Error as a WebDriverError type.  If the given error is already
- * in the WebDriverError prototype chain, this function acts as a no-op.
+ * Wraps any error as a WebDriverError.  If the given error is already in
+ * the WebDriverError prototype chain, this function returns it
+ * unmodified.
  */
 error.wrap = function (err) {
   if (error.isWebDriverError(err)) {
     return err;
   }
-  return new WebDriverError(`${err.name}: ${err.message}`, err.stacktrace);
+  return new WebDriverError(err);
 };
 
 /**
  * Unhandled error reporter.  Dumps the error and its stacktrace to console,
  * and reports error to the Browser Console.
  */
 error.report = function (err) {
-  let msg = `Marionette threw an error: ${error.stringify(err)}`;
+  let msg = "Marionette threw an error: " + error.stringify(err);
   dump(msg + "\n");
   if (Cu.reportError) {
     Cu.reportError(msg);
   }
 };
 
 /**
  * Prettifies an instance of Error and its stacktrace to a string.
@@ -140,269 +141,254 @@ error.pprint = function (strings, ...val
         res.push(s);
       }
     }
   }
   return res.join("");
 };
 
 /**
- * Marshal a WebDriverError prototype to a JSON dictionary.
- *
- * @param {WebDriverError} err
- *     Error to serialise.
- *
- * @return {Object.<string, Object>}
- *     JSON dictionary with the keys "error", "message", and "stacktrace".
- * @throws {TypeError}
- *     If error type is not serialisable.
- */
-error.toJson = function (err) {
-  if (!error.isWebDriverError(err)) {
-    throw new TypeError(`Unserialisable error type: ${err}`);
-  }
-
-  let json = {
-    error: err.status,
-    message: err.message || "",
-    stacktrace: err.stack || "",
-  };
-  return json;
-};
-
-/**
- * Unmarshal a JSON dictionary to a WebDriverError prototype.
- *
- * @param {Object.<string, string>} json
- *     JSON dictionary with the keys "error", "message", and "stacktrace".
- *
- * @return {WebDriverError}
- *     Deserialised error prototype.
- */
-error.fromJson = function (json) {
-  if (!statusLookup.has(json.error)) {
-    throw new TypeError(`Undeserialisable error type: ${json.error}`);
-  }
-
-  let errCls = statusLookup.get(json.error);
-  let err = new errCls(json.message);
-  if ("stacktrace" in json) {
-    err.stack = json.stacktrace;
-  }
-  return err;
-};
-
-/**
  * WebDriverError is the prototypal parent of all WebDriver errors.
  * It should not be used directly, as it does not correspond to a real
  * error in the specification.
- *
- * @param {(string|Error)=} err
- *     Optional string describing error situation or Error instance
- *     to propagate.
  */
-this.WebDriverError = function (err) {
-  const proto = Error.call(this, err);
-  this.name = "WebDriverError";
-  this.status = "webdriver error";
-
-  if (error.isError(err)) {
-    this.message = err.message;
-    this.stack = err.stack;
-  } else {
-    this.message = err;
-    this.stack = proto.stack;
-  }
-};
-WebDriverError.prototype = Object.create(Error.prototype);
-
-this.ElementNotAccessibleError = function (msg) {
-  WebDriverError.call(this, msg);
-  this.name = "ElementNotAccessibleError";
-  this.status = "element not accessible";
-};
-ElementNotAccessibleError.prototype = Object.create(WebDriverError.prototype);
-
-this.ElementNotVisibleError = function (msg) {
-  WebDriverError.call(this, msg);
-  this.name = "ElementNotVisibleError";
-  this.status = "element not visible";
-};
-ElementNotVisibleError.prototype = Object.create(WebDriverError.prototype);
-
-this.InsecureCertificateError = function (msg) {
-  WebDriverError.call(this, msg);
-  this.name = "InsecureCertificateError";
-  this.status = "insecure certificate";
-};
-InsecureCertificateError.prototype = Object.create(WebDriverError.prototype);
-
-this.InvalidArgumentError = function (msg) {
-  WebDriverError.call(this, msg);
-  this.name = "InvalidArgumentError";
-  this.status = "invalid argument";
-};
-InvalidArgumentError.prototype = Object.create(WebDriverError.prototype);
+class WebDriverError extends Error {
+  /**
+   * @param {(string|Error)=} x
+   *     Optional string describing error situation or Error instance
+   *     to propagate.
+   */
+  constructor (x) {
+    super(x);
+    this.name = this.constructor.name;
+    this.status = "webdriver error";
 
-this.InvalidElementStateError = function (msg) {
-  WebDriverError.call(this, msg);
-  this.name = "InvalidElementStateError";
-  this.status = "invalid element state";
-};
-InvalidElementStateError.prototype = Object.create(WebDriverError.prototype);
-
-this.InvalidSelectorError = function (msg) {
-  WebDriverError.call(this, msg);
-  this.name = "InvalidSelectorError";
-  this.status = "invalid selector";
-};
-InvalidSelectorError.prototype = Object.create(WebDriverError.prototype);
-
-this.InvalidSessionIdError = function (msg) {
-  WebDriverError.call(this, msg);
-  this.name = "InvalidSessionIdError";
-  this.status = "invalid session id";
-};
-InvalidSessionIdError.prototype = Object.create(WebDriverError.prototype);
+    // Error's ctor does not preserve x' stack
+    if (error.isError(x)) {
+      this.stack = x.stack;
+    }
+  }
 
-/**
- * Creates an error message for a JavaScript error thrown during
- * executeScript or executeAsyncScript.
- *
- * @param {Error} err
- *     An Error object passed to a catch block or a message.
- * @param {string=} fnName
- *     The name of the function to use in the stack trace message
- *     (e.g. execute_script).
- * @param {string=} file
- *     The filename of the test file containing the Marionette
- *     command that caused this error to occur.
- * @param {number=} line
- *     The line number of the above test file.
- * @param {string=} script
- *     The JS script being executed in text form.
- */
-this.JavaScriptError = function (
-    err, fnName = null, file = null, line = null, script = null) {
-  let msg = String(err);
-  let trace = "";
-
-  if (fnName) {
-    trace += fnName;
-    if (file) {
-      trace += ` @${file}`;
-      if (line) {
-        trace += `, line ${line}`;
-      }
+  toJSON () {
+    return {
+      error: this.status,
+      message: this.message || "",
+      stacktrace: this.stack || "",
     }
   }
 
-  if (typeof err == "object" && "name" in err && "stack" in err) {
-    let jsStack = err.stack.split("\n");
-    let match = jsStack[0].match(/:(\d+):\d+$/);
-    let jsLine = match ? parseInt(match[1]) : 0;
-    if (script) {
-      let src = script.split("\n")[jsLine];
-      trace += "\n" +
-        "inline javascript, line " + jsLine + "\n" +
-        "src: \"" + src + "\"";
+  static fromJSON (json) {
+    if (typeof json.error == "undefined") {
+      let s = JSON.stringify(json);
+      throw new TypeError("Undeserialisable error type: " + s);
+    }
+    if (!STATUSES.has(json.error)) {
+      throw new TypeError("Not of WebDriverError descent: " + json.error);
+    }
+
+    let cls = STATUSES.get(json.error);
+    let err = new cls();
+    if ("message" in json) {
+      err.message = json.message;
+    }
+    if ("stacktrace" in json) {
+      err.stack = json.stacktrace;
     }
-    trace += "\nStack:\n" + String(err.stack);
+    return err;
   }
+}
+
+class ElementNotAccessibleError extends WebDriverError {
+  constructor (message) {
+    super(message);
+    this.status = "element not accessible";
+  }
+}
 
-  WebDriverError.call(this, msg);
-  this.name = "JavaScriptError";
-  this.status = "javascript error";
-  this.stack = trace;
-};
-JavaScriptError.prototype = Object.create(WebDriverError.prototype);
+class ElementNotVisibleError extends WebDriverError {
+  constructor (message) {
+    super(message);
+    this.status = "element not visible";
+  }
+}
+
+class InsecureCertificateError extends WebDriverError {
+  constructor (message) {
+    super(message);
+    this.status = "insecure certificate";
+  }
+}
 
-this.NoAlertOpenError = function (msg) {
-  WebDriverError.call(this, msg);
-  this.name = "NoAlertOpenError";
-  this.status = "no such alert";
-};
-NoAlertOpenError.prototype = Object.create(WebDriverError.prototype);
+class InvalidArgumentError extends WebDriverError {
+  constructor (message) {
+    super(message);
+    this.status = "invalid argument";
+  }
+}
 
-this.NoSuchElementError = function (msg) {
-  WebDriverError.call(this, msg);
-  this.name = "NoSuchElementError";
-  this.status = "no such element";
-};
-NoSuchElementError.prototype = Object.create(WebDriverError.prototype);
+class InvalidElementStateError extends WebDriverError {
+  constructor (message) {
+    super(message);
+    this.status = "invalid element state";
+  }
+}
+
+class InvalidSelectorError extends WebDriverError {
+  constructor (message) {
+    super(message);
+    this.status = "invalid selector";
+  }
+}
 
-this.NoSuchFrameError = function (msg) {
-  WebDriverError.call(this, msg);
-  this.name = "NoSuchFrameError";
-  this.status = "no such frame";
-};
-NoSuchFrameError.prototype = Object.create(WebDriverError.prototype);
+class InvalidSessionIDError extends WebDriverError {
+  constructor (message) {
+    super(message);
+    this.status = "invalid session id";
+  }
+}
 
-this.NoSuchWindowError = function (msg) {
-  WebDriverError.call(this, msg);
-  this.name = "NoSuchWindowError";
-  this.status = "no such window";
-};
-NoSuchWindowError.prototype = Object.create(WebDriverError.prototype);
+/**
+ * Creates a richly annotated error for an error situation that occurred
+ * whilst evaluating injected scripts.
+ */
+class JavaScriptError extends WebDriverError {
+  /**
+   * @param {(string|Error)} x
+   *     An Error object instance or a string describing the error
+   *     situation.
+   * @param {string=} fnName
+   *     Name of the function to use in the stack trace message.
+   * @param {string=} file
+   *     Filename of the test file on the client.
+   * @param {number=} line
+   *     Line number of |file|.
+   * @param {string=} script
+   *     Script being executed, in text form.
+   */
+  constructor (
+      x,
+      fnName = undefined,
+      file = undefined,
+      line = undefined,
+      script = undefined) {
+    let msg = String(x);
+    let trace = "";
 
-this.ScriptTimeoutError = function (msg) {
-  WebDriverError.call(this, msg);
-  this.name = "ScriptTimeoutError";
-  this.status = "script timeout";
-};
-ScriptTimeoutError.prototype = Object.create(WebDriverError.prototype);
+    if (fnName) {
+      trace += fnName;
+      if (file) {
+        trace += ` @${file}`;
+        if (line) {
+          trace += `, line ${line}`;
+        }
+      }
+    }
 
-this.SessionNotCreatedError = function (msg) {
-  WebDriverError.call(this, msg);
-  this.name = "SessionNotCreatedError";
-  this.status = "session not created";
-};
-SessionNotCreatedError.prototype = Object.create(WebDriverError.prototype);
+    if (error.isError(x)) {
+      let jsStack = x.stack.split("\n");
+      let match = jsStack[0].match(/:(\d+):\d+$/);
+      let jsLine = match ? parseInt(match[1]) : 0;
+      if (script) {
+        let src = script.split("\n")[jsLine];
+        trace += "\n" +
+          `inline javascript, line ${jsLine}\n` +
+          `src: "${src}"`;
+      }
+      trace += "\nStack:\n" + x.stack;
+    }
 
-this.StaleElementReferenceError = function (msg) {
-  WebDriverError.call(this, msg);
-  this.name = "StaleElementReferenceError";
-  this.status = "stale element reference";
-};
-StaleElementReferenceError.prototype = Object.create(WebDriverError.prototype);
+    super(msg);
+    this.status = "javascript error";
+    this.stack = trace;
+  }
+}
+
+class NoAlertOpenError extends WebDriverError {
+  constructor (message) {
+    super(message);
+    this.status = "no such alert";
+  }
+}
 
-this.TimeoutError = function (msg) {
-  WebDriverError.call(this, msg);
-  this.name = "TimeoutError";
-  this.status = "timeout";
-};
-TimeoutError.prototype = Object.create(WebDriverError.prototype);
+class NoSuchElementError extends WebDriverError {
+  constructor (message) {
+    super(message);
+    this.status = "no such element";
+  }
+}
+
+class NoSuchFrameError extends WebDriverError {
+  constructor (message) {
+    super(message);
+    this.status = "no such frame";
+  }
+}
 
-this.UnableToSetCookieError = function (msg) {
-  WebDriverError.call(this, msg);
-  this.name = "UnableToSetCookieError";
-  this.status = "unable to set cookie";
-};
-UnableToSetCookieError.prototype = Object.create(WebDriverError.prototype);
+class NoSuchWindowError extends WebDriverError {
+  constructor (message) {
+    super(message);
+    this.status = "no such window";
+  }
+}
+
+class ScriptTimeoutError extends WebDriverError {
+  constructor (message) {
+    super(message);
+    this.status = "script timeout";
+  }
+}
 
-this.UnknownCommandError = function (msg) {
-  WebDriverError.call(this, msg);
-  this.name = "UnknownCommandError";
-  this.status = "unknown command";
-};
-UnknownCommandError.prototype = Object.create(WebDriverError.prototype);
+class SessionNotCreatedError extends WebDriverError {
+  constructor (message) {
+    super(message);
+    this.status = "session not created";
+  }
+}
+
+class StaleElementReferenceError extends WebDriverError {
+  constructor (message) {
+    super(message);
+    this.status = "stale element reference";
+  }
+}
 
-this.UnknownError = function (msg) {
-  WebDriverError.call(this, msg);
-  this.name = "UnknownError";
-  this.status = "unknown error";
-};
-UnknownError.prototype = Object.create(WebDriverError.prototype);
+class TimeoutError extends WebDriverError {
+  constructor (message) {
+    super(message);
+    this.status = "timeout";
+  }
+}
+
+class UnableToSetCookieError extends WebDriverError {
+  constructor (message) {
+    super(message);
+    this.status = "unable to set cookie";
+  }
+}
 
-this.UnsupportedOperationError = function (msg) {
-  WebDriverError.call(this, msg);
-  this.name = "UnsupportedOperationError";
-  this.status = "unsupported operation";
-};
-UnsupportedOperationError.prototype = Object.create(WebDriverError.prototype);
+class UnknownCommandError extends WebDriverError {
+  constructor (message) {
+    super(message);
+    this.status = "unknown command";
+  }
+}
+
+class UnknownError extends WebDriverError {
+  constructor (message) {
+    super(message);
+    this.status = "unknown error";
+  }
+}
+
+class UnsupportedOperationError extends WebDriverError {
+  constructor (message) {
+    super(message);
+    this.status = "unsupported operation";
+  }
+}
 
 const STATUSES = new Map([
   ["element not accessible", ElementNotAccessibleError],
   ["element not visible", ElementNotVisibleError],
   ["insecure certificate", InsecureCertificateError],
   ["invalid argument", InvalidArgumentError],
   ["invalid element state", InvalidElementStateError],
   ["invalid selector", InvalidSelectorError],
--- a/testing/marionette/message.js
+++ b/testing/marionette/message.js
@@ -249,25 +249,22 @@ this.Response = class {
    * @param {Error} err
    *     The Error instance to send.
    *
    * @throws {Error}
    *     If the {@code error} is not a WebDriverError, the error is
    *     propagated.
    */
   sendError(err) {
-    let wd = error.isWebDriverError(err);
-    let we = wd ? err : new WebDriverError(err.message);
-
-    this.error = error.toJson(we);
+    this.error = error.wrap(err).toJSON();
     this.body = null;
     this.send();
 
-    // propagate errors that are implementation problems
-    if (!wd) {
+    // propagate errors which are implementation problems
+    if (!error.isWebDriverError(err)) {
       throw err;
     }
   }
 
   toMsg() {
     return [Response.TYPE, this.id, this.error, this.body];
   }
 
--- a/testing/marionette/proxy.js
+++ b/testing/marionette/proxy.js
@@ -109,17 +109,17 @@ proxy.AsyncMessageChannel = class {
 
         switch (msg.json.type) {
           case proxy.AsyncMessageChannel.ReplyType.Ok:
           case proxy.AsyncMessageChannel.ReplyType.Value:
             resolve(msg.json.data);
             break;
 
           case proxy.AsyncMessageChannel.ReplyType.Error:
-            let err = error.fromJson(msg.json.data);
+            let err = WebDriverError.fromJSON(msg.json.data);
             reject(err);
             break;
 
           default:
             throw new TypeError(
                 `Unknown async response type: ${msg.json.type}`);
         }
       };
@@ -167,26 +167,34 @@ proxy.AsyncMessageChannel = class {
   reply(uuid, obj = undefined) {
     // TODO(ato): Eventually the uuid will be hidden in the dispatcher
     // in listener, and passing it explicitly to this function will be
     // unnecessary.
     if (typeof obj == "undefined") {
       this.sendReply_(uuid, proxy.AsyncMessageChannel.ReplyType.Ok);
     } else if (error.isError(obj)) {
       let err = error.wrap(obj);
-      let serr = error.toJson(err);
-      this.sendReply_(uuid, proxy.AsyncMessageChannel.ReplyType.Error, serr);
+      this.sendReply_(uuid, proxy.AsyncMessageChannel.ReplyType.Error, err);
     } else {
       this.sendReply_(uuid, proxy.AsyncMessageChannel.ReplyType.Value, obj);
     }
   }
 
   sendReply_(uuid, type, data = undefined) {
-    let path = proxy.AsyncMessageChannel.makePath(uuid);
-    let msg = {type: type, data: data};
+    const path = proxy.AsyncMessageChannel.makePath(uuid);
+
+    let payload;
+    if (data && typeof data.toJSON == "function") {
+      payload = data.toJSON();
+    } else {
+      payload = data;
+    }
+
+    const msg = {type: type, data: payload};
+
     // here sendAsync is actually the content frame's
     // sendAsyncMessage(path, message) global
     this.sendAsync(path, msg);
   }
 
   /**
    * Produces a path, or a name, for the message listener handler that
    * listens for a reply.
--- a/testing/marionette/test_assert.js
+++ b/testing/marionette/test_assert.js
@@ -7,17 +7,17 @@
 const {utils: Cu} = Components;
 
 Cu.import("chrome://marionette/content/assert.js");
 Cu.import("chrome://marionette/content/error.js");
 
 add_test(function test_session() {
   assert.session({sessionId: "foo"});
   for (let typ of [null, undefined, ""]) {
-    Assert.throws(() => assert.session({sessionId: typ}), InvalidSessionIdError);
+    Assert.throws(() => assert.session({sessionId: typ}), InvalidSessionIDError);
   }
 
   run_next_test();
 });
 
 add_test(function test_platforms() {
   // at least one will fail
   let raised;
--- a/testing/marionette/test_error.js
+++ b/testing/marionette/test_error.js
@@ -38,16 +38,17 @@ add_test(function test_isWebDriverError(
   notok(error.isWebDriverError(new RangeError()));
   notok(error.isWebDriverError(new ReferenceError()));
   notok(error.isWebDriverError(new SyntaxError()));
   notok(error.isWebDriverError(new TypeError()));
   notok(error.isWebDriverError(new URIError()));
 
   ok(error.isWebDriverError(new WebDriverError()));
   ok(error.isWebDriverError(new InvalidArgumentError()));
+  ok(error.isWebDriverError(new JavaScriptError()));
 
   run_next_test();
 });
 
 add_test(function test_wrap() {
   // webdriver-derived errors should not be wrapped
   equal(error.wrap(new WebDriverError()).name, "WebDriverError");
   ok(error.wrap(new WebDriverError()) instanceof WebDriverError);
@@ -83,145 +84,160 @@ add_test(function test_stringify() {
   equal("WebDriverError: foo",
       error.stringify(new WebDriverError("foo")).split("\n")[0]);
   equal("InvalidArgumentError: foo",
       error.stringify(new InvalidArgumentError("foo")).split("\n")[0]);
 
   run_next_test();
 });
 
-add_test(function test_toJson() {
-  Assert.throws(() => error.toJson(new Error()),
-      /Unserialisable error type: [object Error]/);
-
+add_test(function test_toJSON() {
   let e0 = new WebDriverError();
-  let e0s = error.toJson(e0);
+  let e0s = e0.toJSON();
   equal(e0s.error, "webdriver error");
   equal(e0s.message, "");
   equal(e0s.stacktrace, e0.stack);
 
   let e1 = new WebDriverError("a");
-  let e1s = error.toJson(e1);
+  let e1s = e1.toJSON();
   equal(e1s.message, e1.message);
   equal(e1s.stacktrace, e1.stack);
 
   let e2 = new JavaScriptError("first", "second", "third", "fourth");
-  let e2s = error.toJson(e2);
+  let e2s = e2.toJSON();
   equal(e2.status, e2s.error);
   equal(e2.message, e2s.message);
   ok(e2s.stacktrace.match(/second/));
   ok(e2s.stacktrace.match(/third/));
   ok(e2s.stacktrace.match(/fourth/));
 
   run_next_test();
 });
 
-add_test(function test_fromJson() {
-  Assert.throws(() => error.fromJson({error: "foo"}),
-      /Undeserialisable error type: foo/);
-  Assert.throws(() => error.fromJson({error: "Error"}),
-      /Undeserialisable error type: Error/);
-  Assert.throws(() => error.fromJson({}),
-      /Undeserialisable error type: undefined/);
+add_test(function test_fromJSON() {
+  Assert.throws(() => WebDriverError.fromJSON({error: "foo"}),
+      /Not of WebDriverError descent/);
+  Assert.throws(() => WebDriverError.fromJSON({error: "Error"}),
+      /Not of WebDriverError descent/);
+  Assert.throws(() => WebDriverError.fromJSON({}),
+      /Undeserialisable error type/);
+  Assert.throws(() => WebDriverError.fromJSON(undefined),
+      /TypeError/);
 
   // stacks will be different
   let e1 = new WebDriverError("1");
-  let e1r = error.fromJson({error: "webdriver error", message: "1"});
+  let e1r = WebDriverError.fromJSON({error: "webdriver error", message: "1"});
+  ok(e1r instanceof WebDriverError);
+  equal(e1r.name, e1.name);
   equal(e1r.status, e1.status);
   equal(e1r.message, e1.message);
 
   // stacks will be different
   let e2 = new InvalidArgumentError("2");
-  let e2r = error.fromJson({error: "invalid argument", message: "2"});
+  let e2r = WebDriverError.fromJSON({error: "invalid argument", message: "2"});
+  ok(e2r instanceof WebDriverError);
+  ok(e2r instanceof InvalidArgumentError);
+  equal(e2r.name, e2.name);
   equal(e2r.status, e2.status);
   equal(e2r.message, e2.message);
 
-  let e3 = new JavaScriptError("first", "second", "third", "fourth");
-  let e3s = error.toJson(e3);
-  deepEqual(e3, error.fromJson(e3s));
+  // test stacks
+  let e3j = {error: "no such element", message: "3", stacktrace: "4"};
+  let e3r = WebDriverError.fromJSON(e3j);
+  ok(e3r instanceof WebDriverError);
+  ok(e3r instanceof NoSuchElementError);
+  equal(e3r.name, "NoSuchElementError");
+  equal(e3r.status, e3j.error);
+  equal(e3r.message, e3j.message);
+  equal(e3r.stack, e3j.stacktrace);
+
+  // parity with toJSON
+  let e4 = new JavaScriptError("first", "second", "third", "fourth");
+  let e4s = e4.toJSON();
+  deepEqual(e4, WebDriverError.fromJSON(e4s));
 
   run_next_test();
 });
 
 add_test(function test_WebDriverError() {
   let err = new WebDriverError("foo");
   equal("WebDriverError", err.name);
   equal("foo", err.message);
   equal("webdriver error", err.status);
-  equal(Error.prototype.toString(), Object.getPrototypeOf(err).toString());
+  ok(err instanceof WebDriverError);
 
   run_next_test();
 });
 
 add_test(function test_ElementNotAccessibleError() {
   let err = new ElementNotAccessibleError("foo");
   equal("ElementNotAccessibleError", err.name);
   equal("foo", err.message);
   equal("element not accessible", err.status);
-  equal(WebDriverError.prototype.toString(), Object.getPrototypeOf(err).toString());
+  ok(err instanceof WebDriverError);
 
   run_next_test();
 });
 
 add_test(function test_ElementNotVisibleError() {
   let err = new ElementNotVisibleError("foo");
   equal("ElementNotVisibleError", err.name);
   equal("foo", err.message);
   equal("element not visible", err.status);
-  equal(WebDriverError.prototype.toString(), Object.getPrototypeOf(err).toString());
+  ok(err instanceof WebDriverError);
 
   run_next_test();
 });
 
 add_test(function test_InvalidArgumentError() {
   let err = new InvalidArgumentError("foo");
   equal("InvalidArgumentError", err.name);
   equal("foo", err.message);
   equal("invalid argument", err.status);
-  equal(WebDriverError.prototype.toString(), Object.getPrototypeOf(err).toString());
+  ok(err instanceof WebDriverError);
 
   run_next_test();
 });
 
 add_test(function test_InvalidElementStateError() {
   let err = new InvalidElementStateError("foo");
   equal("InvalidElementStateError", err.name);
   equal("foo", err.message);
   equal("invalid element state", err.status);
-  equal(WebDriverError.prototype.toString(), Object.getPrototypeOf(err).toString());
+  ok(err instanceof WebDriverError);
 
   run_next_test();
 });
 
 add_test(function test_InvalidSelectorError() {
   let err = new InvalidSelectorError("foo");
   equal("InvalidSelectorError", err.name);
   equal("foo", err.message);
   equal("invalid selector", err.status);
-  equal(WebDriverError.prototype.toString(), Object.getPrototypeOf(err).toString());
+  ok(err instanceof WebDriverError);
 
   run_next_test();
 });
 
-add_test(function test_InvalidSessionIdError() {
-  let err = new InvalidSessionIdError("foo");
-  equal("InvalidSessionIdError", err.name);
+add_test(function test_InvalidSessionIDError() {
+  let err = new InvalidSessionIDError("foo");
+  equal("InvalidSessionIDError", err.name);
   equal("foo", err.message);
   equal("invalid session id", err.status);
-  equal(WebDriverError.prototype.toString(), Object.getPrototypeOf(err).toString());
+  ok(err instanceof WebDriverError);
 
   run_next_test();
 });
 
 add_test(function test_JavaScriptError() {
   let err = new JavaScriptError("foo");
   equal("JavaScriptError", err.name);
   equal("foo", err.message);
   equal("javascript error", err.status);
-  equal(WebDriverError.prototype.toString(), Object.getPrototypeOf(err).toString());
+  ok(err instanceof WebDriverError);
 
   equal("undefined", new JavaScriptError(undefined).message);
   // TODO(ato): Bug 1240550
   //equal("funcname @file", new JavaScriptError("message", "funcname", "file").stack);
   equal("funcname @file, line line",
       new JavaScriptError("message", "funcname", "file", "line").stack);
 
   // TODO(ato): More exhaustive tests for JS stack computation
@@ -229,122 +245,122 @@ add_test(function test_JavaScriptError()
   run_next_test();
 });
 
 add_test(function test_NoAlertOpenError() {
   let err = new NoAlertOpenError("foo");
   equal("NoAlertOpenError", err.name);
   equal("foo", err.message);
   equal("no such alert", err.status);
-  equal(WebDriverError.prototype.toString(), Object.getPrototypeOf(err).toString());
+  ok(err instanceof WebDriverError);
 
   run_next_test();
 });
 
 add_test(function test_NoSuchElementError() {
   let err = new NoSuchElementError("foo");
   equal("NoSuchElementError", err.name);
   equal("foo", err.message);
   equal("no such element", err.status);
-  equal(WebDriverError.prototype.toString(), Object.getPrototypeOf(err).toString());
+  ok(err instanceof WebDriverError);
 
   run_next_test();
 });
 
 add_test(function test_NoSuchFrameError() {
   let err = new NoSuchFrameError("foo");
   equal("NoSuchFrameError", err.name);
   equal("foo", err.message);
   equal("no such frame", err.status);
-  equal(WebDriverError.prototype.toString(), Object.getPrototypeOf(err).toString());
+  ok(err instanceof WebDriverError);
 
   run_next_test();
 });
 
 add_test(function test_NoSuchWindowError() {
   let err = new NoSuchWindowError("foo");
   equal("NoSuchWindowError", err.name);
   equal("foo", err.message);
   equal("no such window", err.status);
-  equal(WebDriverError.prototype.toString(), Object.getPrototypeOf(err).toString());
+  ok(err instanceof WebDriverError);
 
   run_next_test();
 });
 
 add_test(function test_ScriptTimeoutError() {
   let err = new ScriptTimeoutError("foo");
   equal("ScriptTimeoutError", err.name);
   equal("foo", err.message);
   equal("script timeout", err.status);
-  equal(WebDriverError.prototype.toString(), Object.getPrototypeOf(err).toString());
+  ok(err instanceof WebDriverError);
 
   run_next_test();
 });
 
 add_test(function test_SessionNotCreatedError() {
   let err = new SessionNotCreatedError("foo");
   equal("SessionNotCreatedError", err.name);
   equal("foo", err.message);
   equal("session not created", err.status);
-  equal(WebDriverError.prototype.toString(), Object.getPrototypeOf(err).toString());
+  ok(err instanceof WebDriverError);
 
   run_next_test();
 });
 
 add_test(function test_StaleElementReferenceError() {
   let err = new StaleElementReferenceError("foo");
   equal("StaleElementReferenceError", err.name);
   equal("foo", err.message);
   equal("stale element reference", err.status);
-  equal(WebDriverError.prototype.toString(), Object.getPrototypeOf(err).toString());
+  ok(err instanceof WebDriverError);
 
   run_next_test();
 });
 
 add_test(function test_TimeoutError() {
   let err = new TimeoutError("foo");
   equal("TimeoutError", err.name);
   equal("foo", err.message);
   equal("timeout", err.status);
-  equal(WebDriverError.prototype.toString(), Object.getPrototypeOf(err).toString());
+  ok(err instanceof WebDriverError);
 
   run_next_test();
 });
 
 add_test(function test_UnableToSetCookieError() {
   let err = new UnableToSetCookieError("foo");
   equal("UnableToSetCookieError", err.name);
   equal("foo", err.message);
   equal("unable to set cookie", err.status);
-  equal(WebDriverError.prototype.toString(), Object.getPrototypeOf(err).toString());
+  ok(err instanceof WebDriverError);
 
   run_next_test();
 });
 
 add_test(function test_UnknownCommandError() {
   let err = new UnknownCommandError("foo");
   equal("UnknownCommandError", err.name);
   equal("foo", err.message);
   equal("unknown command", err.status);
-  equal(WebDriverError.prototype.toString(), Object.getPrototypeOf(err).toString());
+  ok(err instanceof WebDriverError);
 
   run_next_test();
 });
 
 add_test(function test_UnknownError() {
   let err = new UnknownError("foo");
   equal("UnknownError", err.name);
   equal("foo", err.message);
   equal("unknown error", err.status);
-  equal(WebDriverError.prototype.toString(), Object.getPrototypeOf(err).toString());
+  ok(err instanceof WebDriverError);
 
   run_next_test();
 });
 
 add_test(function test_UnsupportedOperationError() {
   let err = new UnsupportedOperationError("foo");
   equal("UnsupportedOperationError", err.name);
   equal("foo", err.message);
   equal("unsupported operation", err.status);
-  equal(WebDriverError.prototype.toString(), Object.getPrototypeOf(err).toString());
+  ok(err instanceof WebDriverError);
 
   run_next_test();
 });
--- a/testing/marionette/test_message.js
+++ b/testing/marionette/test_message.js
@@ -138,17 +138,17 @@ add_test(function test_Response_send() {
   equal(true, fired);
 
   run_next_test();
 });
 
 add_test(function test_Response_sendError() {
   let err = new WebDriverError();
   let resp = new Response(42, r => {
-    equal(error.toJson(err).error, r.error.error);
+    equal(err.toJSON().error, r.error.error);
     equal(null, r.body);
     equal(false, r.sent);
   });
 
   resp.sendError(err);
   equal(true, resp.sent);
   Assert.throws(() => resp.send(), /already been sent/);