Bug 1375212 - Wrap thrown strings in Error objects r=eoger,markh
authorNicolas Ouellet-Payeur <nicolaso@google.com>
Sat, 22 Jul 2017 18:55:43 -0700
changeset 419956 b936f241e649d6873c827aad7abf082fdfa91043
parent 419955 6abd15012dcd2e345ba694ae5480e223502e59b7
child 419957 b435eecc213bace154aabdf832abb749affd854b
push id7566
push usermtabara@mozilla.com
push dateWed, 02 Aug 2017 08:25:16 +0000
treeherdermozilla-beta@86913f512c3c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerseoger, markh
bugs1375212
milestone56.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 1375212 - Wrap thrown strings in Error objects r=eoger,markh MozReview-Commit-ID: KquBcbNhBEN
services/.eslintrc.js
services/common/rest.js
services/common/tests/unit/test_load_modules.js
services/common/tests/unit/test_utils_encodeBase32.js
services/common/utils.js
services/crypto/modules/WeaveCrypto.js
services/fxaccounts/FxAccountsClient.jsm
services/fxaccounts/tests/xpcshell/test_accounts.js
services/sync/modules/engines.js
services/sync/modules/engines/bookmarks.js
services/sync/modules/engines/history.js
services/sync/modules/engines/prefs.js
services/sync/modules/record.js
services/sync/modules/resource.js
services/sync/modules/service.js
services/sync/modules/util.js
services/sync/tests/unit/head_http_server.js
services/sync/tests/unit/test_bookmark_batch_fail.js
services/sync/tests/unit/test_bookmark_engine.js
services/sync/tests/unit/test_collection_getBatched.js
services/sync/tests/unit/test_errorhandler_filelog.js
services/sync/tests/unit/test_records_crypto.js
services/sync/tests/unit/test_resource.js
services/sync/tests/unit/test_resource_async.js
services/sync/tests/unit/test_service_sync_locked.js
services/sync/tests/unit/test_syncengine_sync.js
services/sync/tests/unit/test_telemetry.js
services/sync/tests/unit/test_utils_catch.js
services/sync/tests/unit/test_utils_lock.js
services/sync/tests/unit/test_utils_notify.js
services/sync/tps/extensions/mozmill/resource/stdlib/EventUtils.js
services/sync/tps/extensions/mozmill/resource/stdlib/httpd.js
services/sync/tps/extensions/tps/resource/modules/history.jsm
services/sync/tps/extensions/tps/resource/quit.js
toolkit/components/extensions/test/xpcshell/test_ext_storage_sync_crypto.js
--- a/services/.eslintrc.js
+++ b/services/.eslintrc.js
@@ -1,7 +1,10 @@
 "use strict";
 
 module.exports = {
   plugins: [
     "mozilla"
-  ]
+  ],
+  "rules": {
+    "no-throw-literal": 2,
+  },
 }
--- a/services/common/rest.js
+++ b/services/common/rest.js
@@ -276,33 +276,33 @@ RESTRequest.prototype = {
     return this.dispatch("DELETE", null, onComplete, onProgress);
   },
 
   /**
    * Abort an active request.
    */
   abort: function abort() {
     if (this.status != this.SENT && this.status != this.IN_PROGRESS) {
-      throw "Can only abort a request that has been sent.";
+      throw new Error("Can only abort a request that has been sent.");
     }
 
     this.status = this.ABORTED;
     this.channel.cancel(Cr.NS_BINDING_ABORTED);
 
     if (this.timeoutTimer) {
       // Clear the abort timer now that the channel is done.
       this.timeoutTimer.clear();
     }
   },
 
   /** Implementation stuff **/
 
   dispatch: function dispatch(method, data, onComplete, onProgress) {
     if (this.status != this.NOT_SENT) {
-      throw "Request has already been sent!";
+      throw new Error("Request has already been sent!");
     }
 
     this.method = method;
     if (onComplete) {
       this.onComplete = onComplete;
     }
     if (onProgress) {
       this.onProgress = onProgress;
--- a/services/common/tests/unit/test_load_modules.js
+++ b/services/common/tests/unit/test_load_modules.js
@@ -29,32 +29,32 @@ function expectImportsToSucceed(mm, base
     let resource = base + m;
     let succeeded = false;
     try {
       Components.utils.import(resource, {});
       succeeded = true;
     } catch (e) {}
 
     if (!succeeded) {
-      throw "Importing " + resource + " should have succeeded!";
+      throw new Error(`Importing ${resource} should have succeeded!`);
     }
   }
 }
 
 function expectImportsToFail(mm, base = MODULE_BASE) {
   for (let m of mm) {
     let resource = base + m;
     let succeeded = false;
     try {
       Components.utils.import(resource, {});
       succeeded = true;
     } catch (e) {}
 
     if (succeeded) {
-      throw "Importing " + resource + " should have failed!";
+      throw new Error(`Importing ${resource} should have failed!`);
     }
   }
 }
 
 function run_test() {
   expectImportsToSucceed(shared_modules);
   expectImportsToSucceed(shared_test_modules, TEST_BASE);
 
--- a/services/common/tests/unit/test_utils_encodeBase32.js
+++ b/services/common/tests/unit/test_utils_encodeBase32.js
@@ -42,10 +42,10 @@ function run_test() {
 
   // Test failure.
   let err;
   try {
     CommonUtils.decodeBase32("000");
   } catch (ex) {
     err = ex;
   }
-  do_check_eq(err, "Unknown character in base32: 0");
+  do_check_eq(err.message, "Unknown character in base32: 0");
 }
--- a/services/common/utils.js
+++ b/services/common/utils.js
@@ -150,17 +150,18 @@ this.CommonUtils = {
 
   /**
    * Return a timer that is scheduled to call the callback after waiting the
    * provided time or as soon as possible. The timer will be set as a property
    * of the provided object with the given timer name.
    */
   namedTimer: function namedTimer(callback, wait, thisObj, name) {
     if (!thisObj || !name) {
-      throw "You must provide both an object and a property name for the timer!";
+      throw new Error(
+          "You must provide both an object and a property name for the timer!");
     }
 
     // Delay an existing timer if it exists
     if (name in thisObj && thisObj[name] instanceof Ci.nsITimer) {
       thisObj[name].delay = wait;
       return thisObj[name];
     }
 
@@ -298,20 +299,20 @@ this.CommonUtils = {
       //   undefined | foo == foo.
       function accumulate(val) {
         ret[rOffset] |= val;
       }
 
       function advance() {
         c  = str[cOffset++];
         if (!c || c == "" || c == "=") // Easier than range checking.
-          throw "Done";                // Will be caught far away.
+          throw new Error("Done");     // Will be caught far away.
         val = key.indexOf(c);
         if (val == -1)
-          throw "Unknown character in base32: " + c;
+          throw new Error(`Unknown character in base32: ${c}`);
       }
 
       // Handle a left shift, restricted to bytes.
       function left(octet, shift) {
         return (octet << shift) & 0xff;
       }
 
       advance();
@@ -347,17 +348,17 @@ this.CommonUtils = {
     let cOff = 0;
     let rOff = 0;
 
     for (; i < blocks; ++i) {
       try {
         processBlock(ret, cOff, rOff);
       } catch (ex) {
         // Handle the detection of padding.
-        if (ex == "Done")
+        if (ex.message == "Done")
           break;
         throw ex;
       }
       cOff += 8;
       rOff += 5;
     }
 
     // Slice in case our shift overflowed to the right.
--- a/services/crypto/modules/WeaveCrypto.js
+++ b/services/crypto/modules/WeaveCrypto.js
@@ -102,17 +102,18 @@ WeaveCrypto.prototype = {
         ivStr = atob(ivStr);
 
         if (operation !== OPERATIONS.ENCRYPT && operation !== OPERATIONS.DECRYPT) {
             throw new Error("Unsupported operation in _commonCrypt.");
         }
         // We never want an IV longer than the block size, which is 16 bytes
         // for AES, neither do we want one smaller; throw in both cases.
         if (ivStr.length !== AES_CBC_IV_SIZE) {
-            throw "Invalid IV size; must be " + AES_CBC_IV_SIZE + " bytes.";
+            throw new Error(
+                `Invalid IV size; must be ${AES_CBC_IV_SIZE} bytes.`);
         }
 
         let iv = this.byteCompressInts(ivStr);
         let symKey = this.importSymKey(symKeyStr, operation);
         let cryptMethod = (operation === OPERATIONS.ENCRYPT
                            ? crypto.subtle.encrypt
                            : crypto.subtle.decrypt)
                           .bind(crypto.subtle);
@@ -171,17 +172,17 @@ WeaveCrypto.prototype = {
         switch (operation) {
             case OPERATIONS.ENCRYPT:
                 memo = this._encryptionSymKeyMemo;
                 break;
             case OPERATIONS.DECRYPT:
                 memo = this._decryptionSymKeyMemo;
                 break;
             default:
-                throw "Unsupported operation in importSymKey.";
+                throw new Error("Unsupported operation in importSymKey.");
         }
 
         if (encodedKeyString in memo)
             return memo[encodedKeyString];
 
         let symmetricKeyBuffer = this.makeUint8Array(encodedKeyString, true);
         let algo = { name: CRYPT_ALGO };
         let usages = [operation === OPERATIONS.ENCRYPT ? "encrypt" : "decrypt"];
--- a/services/fxaccounts/FxAccountsClient.jsm
+++ b/services/fxaccounts/FxAccountsClient.jsm
@@ -594,16 +594,17 @@ this.FxAccountsClient.prototype = {
          );
       }
       throw error;
     }
     try {
       return JSON.parse(response.body);
     } catch (error) {
       log.error("json parse error on response: " + response.body);
+      // eslint-disable-next-line no-throw-literal
       throw {error};
     }
   },
 };
 
 function isInvalidTokenError(error) {
   if (error.code != 401) {
     return false;
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -116,17 +116,17 @@ function MockFxAccountsClient() {
     });
   };
 
   this.resendVerificationEmail = function(sessionToken) {
     // Return the session token to show that we received it in the first place
     return Promise.resolve(sessionToken);
   };
 
-  this.signCertificate = function() { throw "no" };
+  this.signCertificate = function() { throw new Error("no"); };
 
   this.signOut = () => Promise.resolve();
   this.signOutAndDestroyDevice = () => Promise.resolve({});
 
   FxAccountsClient.apply(this);
 }
 MockFxAccountsClient.prototype = {
   __proto__: FxAccountsClient.prototype
@@ -1163,17 +1163,20 @@ add_task(async function test_sign_out_wi
 
   await promise;
 });
 
 add_task(async function test_sign_out_with_remote_error() {
   let fxa = new MockFxAccounts();
   let remoteSignOutCalled = false;
   // Force remote sign out to trigger an error
-  fxa.internal.deleteDeviceRegistration = function() { remoteSignOutCalled = true; throw "Remote sign out error"; };
+  fxa.internal.deleteDeviceRegistration = function() {
+    remoteSignOutCalled = true;
+    throw new Error("Remote sign out error");
+  };
   let promiseLogout = new Promise(resolve => {
     makeObserver(ONLOGOUT_NOTIFICATION, function() {
       log.debug("test_sign_out_with_remote_error observed onlogout");
       resolve();
     });
   });
 
   let jane = getTestUser("jane");
--- a/services/sync/modules/engines.js
+++ b/services/sync/modules/engines.js
@@ -377,57 +377,57 @@ Store.prototype = {
    *
    * This is called by the default implementation of applyIncoming(). If using
    * applyIncomingBatch(), this won't be called unless your store calls it.
    *
    * @param record
    *        The store record to create an item from
    */
   async create(record) {
-    throw "override create in a subclass";
+    throw new Error("override create in a subclass");
   },
 
   /**
    * Remove an item in the store from a record.
    *
    * This is called by the default implementation of applyIncoming(). If using
    * applyIncomingBatch(), this won't be called unless your store calls it.
    *
    * @param record
    *        The store record to delete an item from
    */
   async remove(record) {
-    throw "override remove in a subclass";
+    throw new Error("override remove in a subclass");
   },
 
   /**
    * Update an item from a record.
    *
    * This is called by the default implementation of applyIncoming(). If using
    * applyIncomingBatch(), this won't be called unless your store calls it.
    *
    * @param record
    *        The record to use to update an item from
    */
   async update(record) {
-    throw "override update in a subclass";
+    throw new Error("override update in a subclass");
   },
 
   /**
    * Determine whether a record with the specified ID exists.
    *
    * Takes a string record ID and returns a booleans saying whether the record
    * exists.
    *
    * @param  id
    *         string record ID
    * @return boolean indicating whether record exists locally
    */
   async itemExists(id) {
-    throw "override itemExists in a subclass";
+    throw new Error("override itemExists in a subclass");
   },
 
   /**
    * Create a record from the specified ID.
    *
    * If the ID is known, the record should be populated with metadata from
    * the store. If the ID is not known, the record should be created with the
    * delete field set to true.
@@ -435,53 +435,53 @@ Store.prototype = {
    * @param  id
    *         string record ID
    * @param  collection
    *         Collection to add record to. This is typically passed into the
    *         constructor for the newly-created record.
    * @return record type for this engine
    */
   async createRecord(id, collection) {
-    throw "override createRecord in a subclass";
+    throw new Error("override createRecord in a subclass");
   },
 
   /**
    * Change the ID of a record.
    *
    * @param  oldID
    *         string old/current record ID
    * @param  newID
    *         string new record ID
    */
   async changeItemID(oldID, newID) {
-    throw "override changeItemID in a subclass";
+    throw new Error("override changeItemID in a subclass");
   },
 
   /**
    * Obtain the set of all known record IDs.
    *
    * @return Object with ID strings as keys and values of true. The values
    *         are ignored.
    */
   async getAllIDs() {
-    throw "override getAllIDs in a subclass";
+    throw new Error("override getAllIDs in a subclass");
   },
 
   /**
    * Wipe all data in the store.
    *
    * This function is called during remote wipes or when replacing local data
    * with remote data.
    *
    * This function should delete all local data that the store is managing. It
    * can be thought of as clearing out all state and restoring the "new
    * browser" state.
    */
   async wipe() {
-    throw "override wipe in a subclass";
+    throw new Error("override wipe in a subclass");
   }
 };
 
 this.EngineManager = function EngineManager(service) {
   this.service = service;
 
   this._engines = {};
 
@@ -711,28 +711,28 @@ Engine.prototype = {
   },
 
   async sync() {
     if (!this.enabled) {
       return false;
     }
 
     if (!this._sync) {
-      throw "engine does not implement _sync method";
+      throw new Error("engine does not implement _sync method");
     }
 
     return this._notify("sync", this.name, this._sync)();
   },
 
   /**
    * Get rid of any local meta-data.
    */
   async resetClient() {
     if (!this._resetClient) {
-      throw "engine does not implement _resetClient method";
+      throw new Error("engine does not implement _resetClient method");
     }
 
     return this._notify("reset-client", this.name, this._resetClient)();
   },
 
   async _wipeClient() {
     await this.resetClient();
     this._log.debug("Deleting all local data");
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -458,16 +458,17 @@ BookmarksEngine.prototype = {
     }
     try {
       return this._guidMap = await this._buildGUIDMap();
     } catch (ex) {
       if (Async.isShutdownException(ex)) {
         throw ex;
       }
       this._log.warn("Error while building GUID map, skipping all other incoming items", ex);
+      // eslint-disable-next-line no-throw-literal
       throw {code: Engine.prototype.eEngineAbortApplyIncoming,
              cause: ex};
     }
   },
 
   async _deletePending() {
     // Delete pending items -- See the comment above BookmarkStore's deletePending
     let newlyModified = await this._store.deletePending();
@@ -672,17 +673,18 @@ BookmarksStore.prototype = {
         !record.bmkUri) {
       this._log.warn("Skipping malformed query bookmark: " + record.id);
       return;
     }
 
     // Figure out the local id of the parent GUID if available
     let parentGUID = record.parentid;
     if (!parentGUID) {
-      throw "Record " + record.id + " has invalid parentid: " + parentGUID;
+      throw new Error(
+          `Record ${record.id} has invalid parentid: ${parentGUID}`);
     }
     this._log.debug("Remote parent is " + parentGUID);
 
     // Do the normal processing of incoming records
     await Store.prototype.applyIncoming.call(this, record);
 
     if (record.type == "folder" && record.children) {
       this._childrenToOrder[record.id] = record.children;
--- a/services/sync/modules/engines/history.js
+++ b/services/sync/modules/engines/history.js
@@ -315,17 +315,17 @@ HistoryStore.prototype = {
    * returns true if the record is to be applied, false otherwise
    * (no visits to add, etc.),
    */
   _recordToPlaceInfo: function _recordToPlaceInfo(record) {
     // Sort out invalid URIs and ones Places just simply doesn't want.
     record.uri = Utils.makeURI(record.histUri);
     if (!record.uri) {
       this._log.warn("Attempted to process invalid URI, skipping.");
-      throw "Invalid URI in record";
+      throw new Error("Invalid URI in record");
     }
 
     if (!Utils.checkGUID(record.id)) {
       this._log.warn("Encountered record with invalid GUID: " + record.id);
       return false;
     }
     record.guid = record.id;
 
--- a/services/sync/modules/engines/prefs.js
+++ b/services/sync/modules/engines/prefs.js
@@ -148,17 +148,17 @@ PrefStore.prototype = {
         default:
           if (value == null) {
             // Pref has gone missing. The best we can do is reset it.
             this._prefs.reset(pref);
           } else {
             try {
               this._prefs.set(pref, value);
             } catch (ex) {
-              this._log.trace("Failed to set pref: " + pref + ": " + ex);
+              this._log.trace(`Failed to set pref: ${pref}`, ex);
             }
           }
       }
     }
 
     // Notify the lightweight theme manager if the selected theme has changed.
     if (selectedThemeIDBefore != selectedThemeIDAfter) {
       this._updateLightWeightTheme(selectedThemeIDAfter);
--- a/services/sync/modules/record.js
+++ b/services/sync/modules/record.js
@@ -115,17 +115,17 @@ this.CryptoWrapper = function CryptoWrap
 }
 CryptoWrapper.prototype = {
   __proto__: WBORecord.prototype,
   _logName: "Sync.Record.CryptoWrapper",
 
   ciphertextHMAC: function ciphertextHMAC(keyBundle) {
     let hasher = keyBundle.sha256HMACHasher;
     if (!hasher) {
-      throw "Cannot compute HMAC without an HMAC key.";
+      throw new Error("Cannot compute HMAC without an HMAC key.");
     }
 
     return Utils.bytesAsHex(Utils.digestUTF8(this.ciphertext, hasher));
   },
 
   /*
    * Don't directly use the sync key. Instead, grab a key for this
    * collection, which is decrypted with the sync key.
@@ -145,17 +145,17 @@ CryptoWrapper.prototype = {
                                            keyBundle.encryptionKeyB64, this.IV);
     this.hmac = this.ciphertextHMAC(keyBundle);
     this.cleartext = null;
   },
 
   // Optional key bundle.
   decrypt: function decrypt(keyBundle) {
     if (!this.ciphertext) {
-      throw "No ciphertext: nothing to decrypt?";
+      throw new Error("No ciphertext: nothing to decrypt?");
     }
 
     if (!keyBundle) {
       throw new Error("A key bundle must be supplied to decrypt.");
     }
 
     // Authenticate the encrypted blob with the expected HMAC
     let computedHMAC = this.ciphertextHMAC(keyBundle);
@@ -168,22 +168,24 @@ CryptoWrapper.prototype = {
     let cleartext = Weave.Crypto.decrypt(this.ciphertext,
                                          keyBundle.encryptionKeyB64, this.IV);
     let json_result = JSON.parse(cleartext);
 
     if (json_result && (json_result instanceof Object)) {
       this.cleartext = json_result;
       this.ciphertext = null;
     } else {
-      throw "Decryption failed: result is <" + json_result + ">, not an object.";
+      throw new Error(
+          `Decryption failed: result is <${json_result}>, not an object.`);
     }
 
     // Verify that the encrypted id matches the requested record's id.
     if (this.cleartext.id != this.id)
-      throw "Record id mismatch: " + this.cleartext.id + " != " + this.id;
+      throw new Error(
+          `Record id mismatch: ${this.cleartext.id} != ${this.id}`);
 
     return this.cleartext;
   },
 
   cleartextToString() {
     return JSON.stringify(this.cleartext);
   },
 
@@ -493,22 +495,22 @@ CollectionKeyManager.prototype = {
   setContents: function setContents(payload, modified) {
 
     let self = this;
 
     this._log.info("Setting collection keys contents. Our last modified: " +
                    this.lastModified + ", input modified: " + modified + ".");
 
     if (!payload)
-      throw "No payload in CollectionKeyManager.setContents().";
+      throw new Error("No payload in CollectionKeyManager.setContents().");
 
     if (!payload.default) {
       this._log.warn("No downloaded default key: this should not occur.");
       this._log.warn("Not clearing local keys.");
-      throw "No default key in CollectionKeyManager.setContents(). Cannot proceed.";
+      throw new Error("No default key in CollectionKeyManager.setContents(). Cannot proceed.");
     }
 
     // Process the incoming default key.
     let b = new BulkKeyBundle(DEFAULT_KEYBUNDLE_NAME);
     b.keyPairB64 = payload.default;
     let newDefault = b;
 
     // Process the incoming collections.
@@ -563,17 +565,17 @@ CollectionKeyManager.prototype = {
     // storage_keys is a WBO, fetched from storage/crypto/keys.
     // Its payload is the default key, and a map of collections to keys.
     // We lazily compute the key objects from the strings we're given.
 
     let payload;
     try {
       payload = storage_keys.decrypt(syncKeyBundle);
     } catch (ex) {
-      log.warn("Got exception \"" + ex + "\" decrypting storage keys with sync key.");
+      log.warn("Got exception decrypting storage keys with sync key.", ex);
       log.info("Aborting updateContents. Rethrowing.");
       throw ex;
     }
 
     let r = this.setContents(payload, storage_keys.modified);
     log.info("Collection keys updated.");
     return r;
   }
--- a/services/sync/modules/resource.js
+++ b/services/sync/modules/resource.js
@@ -210,17 +210,17 @@ AsyncResource.prototype = {
       let listener = new ChannelListener(this._onComplete, this._onProgress,
                                          this._log, this.ABORT_TIMEOUT);
       channel.requestMethod = action;
       channel.asyncOpen2(listener);
     });
   },
 
   _onComplete(ex, data, channel) {
-    this._log.trace("In _onComplete. Error is " + ex + ".");
+    this._log.trace("In _onComplete. An error occurred.", ex);
 
     if (ex) {
       if (!Async.isShutdownException(ex)) {
         this._log.warn("${action} request to ${url} failed: ${ex}",
                        { action: this.method, url: this.uri.spec, ex});
       }
       this._deferred.reject(ex);
       return;
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -252,18 +252,18 @@ Sync11Service.prototype = {
       } else {
         this._log.warn("Got error response re-uploading keys. " +
                        "Continuing sync; let's try again later.");
       }
 
       return false;            // Don't try again: same keys.
 
     } catch (ex) {
-      this._log.warn("Got exception \"" + ex + "\" fetching and handling " +
-                     "crypto keys. Will try again later.");
+      this._log.warn("Got exception fetching and handling crypto keys. " +
+                     "Will try again later.", ex);
       return false;
     }
   },
 
   async handleFetchedKeys(syncKey, cryptoKeys, skipReset) {
     // Don't want to wipe if we're just starting up!
     let wasBlank = this.collectionKeys.isClear;
     let keysChanged = this.collectionKeys.updateContents(syncKey, cryptoKeys);
@@ -570,17 +570,17 @@ Sync11Service.prototype = {
             } else {
               // Some other problem.
               this.status.login = LOGIN_FAILED_SERVER_ERROR;
               this.errorHandler.checkServerError(cryptoResp);
               this._log.warn("Got status " + cryptoResp.status + " fetching crypto keys.");
               return false;
             }
           } catch (ex) {
-            this._log.warn("Got exception \"" + ex + "\" fetching cryptoKeys.");
+            this._log.warn("Got exception fetching cryptoKeys.", ex);
             // TODO: Um, what exceptions might we get here? Should we re-throw any?
 
             // One kind of exception: HMAC failure.
             if (Utils.isHMACMismatch(ex)) {
               this.status.login = LOGIN_FAILED_INVALID_PASSPHRASE;
               this.status.sync = CREDENTIALS_CHANGED;
             } else {
               // In the absence of further disambiguation or more precise
@@ -821,33 +821,33 @@ Sync11Service.prototype = {
     }
   },
 
   async login() {
     async function onNotify() {
       this._loggedIn = false;
       if (Services.io.offline) {
         this.status.login = LOGIN_FAILED_NETWORK_ERROR;
-        throw "Application is offline, login should not be called";
+        throw new Error("Application is offline, login should not be called");
       }
 
       this._log.info("Logging in the user.");
       // Just let any errors bubble up - they've more context than we do!
       try {
         await this.identity.ensureLoggedIn();
       } finally {
         this._checkSetup(); // _checkSetup has a side effect of setting the right state.
       }
 
       this._updateCachedURLs();
 
       this._log.info("User logged in successfully - verifying login.");
       if (!(await this.verifyLogin())) {
         // verifyLogin sets the failure states here.
-        throw "Login failed: " + this.status.login;
+        throw new Error(`Login failed: ${this.status.login}`);
       }
 
       this._loggedIn = true;
 
       return true;
     }
 
     let notifier = this._notify("login", "", onNotify.bind(this));
@@ -1369,17 +1369,17 @@ Sync11Service.prototype = {
    *        Callback function with signature (error, data) where `data' is
    *        the return value from the server already parsed as JSON.
    *
    * @return RESTRequest instance representing the request, allowing callers
    *         to cancel the request.
    */
   getStorageInfo: function getStorageInfo(type, callback) {
     if (STORAGE_INFO_TYPES.indexOf(type) == -1) {
-      throw "Invalid value for 'type': " + type;
+      throw new Error(`Invalid value for 'type': ${type}`);
     }
 
     let info_type = "info/" + type;
     this._log.trace("Retrieving '" + info_type + "'...");
     let url = this.userBaseURL + info_type;
     return this.getStorageRequest(url).get(function onComplete(error) {
       // Note: 'this' is the request.
       if (error) {
--- a/services/sync/modules/util.js
+++ b/services/sync/modules/util.js
@@ -19,19 +19,35 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 // FxAccountsCommon.js doesn't use a "namespace", so create one here.
 XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function() {
   let FxAccountsCommon = {};
   Cu.import("resource://gre/modules/FxAccountsCommon.js", FxAccountsCommon);
   return FxAccountsCommon;
 });
 
 /*
+ * Custom exception types.
+ */
+class LockException extends Error {
+  constructor(message) {
+    super(message);
+    this.name = "LockException";
+  }
+}
+
+class HMACMismatch extends Error {
+  constructor(message) {
+    super(message);
+    this.name = "HMACMismatch";
+  }
+}
+
+/*
  * Utility functions
  */
-
 this.Utils = {
   // Alias in functions from CommonUtils. These previously were defined here.
   // In the ideal world, references to these would be removed.
   nextTick: CommonUtils.nextTick,
   namedTimer: CommonUtils.namedTimer,
   makeURI: CommonUtils.makeURI,
   encodeUTF8: CommonUtils.encodeUTF8,
   decodeUTF8: CommonUtils.decodeUTF8,
@@ -94,40 +110,44 @@ this.Utils = {
         if (exceptionCallback) {
           return exceptionCallback.call(thisArg, ex);
         }
         return null;
       }
     };
   },
 
+  throwLockException(label) {
+    throw new LockException(`Could not acquire lock. Label: "${label}".`);
+  },
+
   /**
    * Wrap a [promise-returning] function to call lock before calling the function
    * then unlock when it finishes executing or if it threw an error.
    *
    * @usage MyObj._lock = Utils.lock;
    *        MyObj.foo = async function() { await this._lock(func)(); }
    */
   lock(label, func) {
     let thisArg = this;
     return async function WrappedLock() {
       if (!thisArg.lock()) {
-        throw "Could not acquire lock. Label: \"" + label + "\".";
+        Utils.throwLockException(label);
       }
 
       try {
         return await func.call(thisArg);
       } finally {
         thisArg.unlock();
       }
     };
   },
 
   isLockException: function isLockException(ex) {
-    return ex && ex.indexOf && ex.indexOf("Could not acquire lock.") == 0;
+    return ex instanceof LockException;
   },
 
   /**
    * Wrap [promise-returning] functions to notify when it starts and
    * finishes executing or if it threw an error.
    *
    * The message is a combination of a provided prefix, the local name, and
    * the event. Possible events are: "start", "finish", "error". The subject
@@ -239,22 +259,22 @@ this.Utils = {
 
     return true;
   },
 
   // Generator and discriminator for HMAC exceptions.
   // Split these out in case we want to make them richer in future, and to
   // avoid inevitable confusion if the message changes.
   throwHMACMismatch: function throwHMACMismatch(shouldBe, is) {
-    throw "Record SHA256 HMAC mismatch: should be " + shouldBe + ", is " + is;
+    throw new HMACMismatch(
+        `Record SHA256 HMAC mismatch: should be ${shouldBe}, is ${is}`);
   },
 
   isHMACMismatch: function isHMACMismatch(ex) {
-    const hmacFail = "Record SHA256 HMAC mismatch: ";
-    return ex && ex.indexOf && (ex.indexOf(hmacFail) == 0);
+    return ex instanceof HMACMismatch;
   },
 
   /**
    * Turn RFC 4648 base32 into our own user-friendly version.
    *   ABCDEFGHIJKLMNOPQRSTUVWXYZ234567
    * becomes
    *   abcdefghijk8mn9pqrstuvwxyz234567
    */
--- a/services/sync/tests/unit/head_http_server.js
+++ b/services/sync/tests/unit/head_http_server.js
@@ -60,17 +60,17 @@ function httpd_basic_auth_handler(body, 
   response.bodyOutputStream.write(body, body.length);
 }
 
 /*
  * Represent a WBO on the server
  */
 function ServerWBO(id, initialPayload, modified) {
   if (!id) {
-    throw "No ID for ServerWBO!";
+    throw new Error("No ID for ServerWBO!");
   }
   this.id = id;
   if (!initialPayload) {
     return;
   }
 
   if (typeof initialPayload == "object") {
     initialPayload = JSON.stringify(initialPayload);
@@ -530,17 +530,17 @@ function track_collections_helper() {
    */
   function info_collections(request, response) {
     let body = "Error.";
     switch (request.method) {
       case "GET":
         body = JSON.stringify(collections);
         break;
       default:
-        throw "Non-GET on info_collections.";
+        throw new Error("Non-GET on info_collections.");
     }
 
     response.setHeader("Content-Type", "application/json");
     response.setHeader("X-Weave-Timestamp",
                        "" + new_timestamp(),
                        false);
     response.setStatusLine(request.httpVersion, 200, "OK");
     response.bodyOutputStream.write(body, body.length);
@@ -1025,17 +1025,17 @@ SyncServer.prototype = {
             // Rather than instantiate each WBO's handler function, do it once
             // per request. They get hit far less often than do collections.
             wbo.handler()(req, resp);
             coll.timestamp = resp.newModified;
             return resp;
           }
           return coll.collectionHandler(req, resp);
         default:
-          throw "Request method " + req.method + " not implemented.";
+          throw new Error("Request method " + req.method + " not implemented.");
       }
     },
 
     "info": function handleInfo(handler, req, resp, version, username, rest) {
       switch (rest) {
         case "collections":
           let body = JSON.stringify(this.infoCollections(username));
           this.respond(req, resp, 200, "OK", body, {
--- a/services/sync/tests/unit/test_bookmark_batch_fail.js
+++ b/services/sync/tests/unit/test_bookmark_batch_fail.js
@@ -4,20 +4,20 @@
 _("Making sure a failing sync reports a useful error");
 Cu.import("resource://services-sync/engines/bookmarks.js");
 Cu.import("resource://services-sync/service.js");
 
 add_task(async function run_test() {
   let engine = new BookmarksEngine(Service);
   await engine.initialize();
   engine._syncStartup = async function() {
-    throw "FAIL!";
+    throw new Error("FAIL!");
   };
 
   try {
     _("Try calling the sync that should throw right away");
     await engine._sync();
     do_throw("Should have failed sync!");
   } catch (ex) {
     _("Making sure what we threw ended up as the exception:", ex);
-    do_check_eq(ex, "FAIL!");
+    do_check_eq(ex.message, "FAIL!");
   }
 });
--- a/services/sync/tests/unit/test_bookmark_engine.js
+++ b/services/sync/tests/unit/test_bookmark_engine.js
@@ -154,17 +154,17 @@ add_task(async function test_processInco
     folder1_payload.children.reverse();
     collection.insert(folder1_guid, encryptPayload(folder1_payload));
 
     // Create a bogus record that when synced down will provoke a
     // network error which in turn provokes an exception in _processIncoming.
     const BOGUS_GUID = "zzzzzzzzzzzz";
     let bogus_record = collection.insert(BOGUS_GUID, "I'm a bogus record!");
     bogus_record.get = function get() {
-      throw "Sync this!";
+      throw new Error("Sync this!");
     };
 
     // Make the 10 minutes old so it will only be synced in the toFetch phase.
     bogus_record.modified = Date.now() / 1000 - 60 * 10;
     engine.lastSync = Date.now() / 1000 - 60;
     engine.toFetch = [BOGUS_GUID];
 
     let error;
--- a/services/sync/tests/unit/test_collection_getBatched.js
+++ b/services/sync/tests/unit/test_collection_getBatched.js
@@ -34,17 +34,17 @@ function get_test_collection_info({ tota
     }
     requests.push({
       limit,
       offset,
       spec: this.spec,
       headers: Object.assign({}, this.headers)
     });
     if (--throwAfter === 0) {
-      throw "Some Network Error";
+      throw new Error("Some Network Error");
     }
     let body = recordRange(limit, offset, totalRecords);
     let response = {
       obj: body,
       success: true,
       status: 200,
       headers: {}
     };
--- a/services/sync/tests/unit/test_errorhandler_filelog.js
+++ b/services/sync/tests/unit/test_errorhandler_filelog.js
@@ -117,17 +117,17 @@ add_test(function test_logOnSuccess_true
     readFile(logfile, function(error, data) {
       do_check_true(Components.isSuccessCode(error));
       do_check_neq(data.indexOf(MESSAGE), -1);
 
       // Clean up.
       try {
         logfile.remove(false);
       } catch (ex) {
-        dump("Couldn't delete file: " + ex + "\n");
+        dump("Couldn't delete file: " + ex.message + "\n");
         // Stupid Windows box.
       }
 
       Svc.Prefs.resetBranch("");
       run_next_test();
     });
   });
 
@@ -184,17 +184,17 @@ add_test(function test_sync_error_logOnE
     readFile(logfile, function(error, data) {
       do_check_true(Components.isSuccessCode(error));
       do_check_neq(data.indexOf(MESSAGE), -1);
 
       // Clean up.
       try {
         logfile.remove(false);
       } catch (ex) {
-        dump("Couldn't delete file: " + ex + "\n");
+        dump("Couldn't delete file: " + ex.message + "\n");
         // Stupid Windows box.
       }
 
       Svc.Prefs.resetBranch("");
     });
   });
 
   // Fake an unsuccessful sync due to prolonged failure.
@@ -251,17 +251,17 @@ add_test(function test_login_error_logOn
     readFile(logfile, function(error, data) {
       do_check_true(Components.isSuccessCode(error));
       do_check_neq(data.indexOf(MESSAGE), -1);
 
       // Clean up.
       try {
         logfile.remove(false);
       } catch (ex) {
-        dump("Couldn't delete file: " + ex + "\n");
+        dump("Couldn't delete file: " + ex.message + "\n");
         // Stupid Windows box.
       }
 
       Svc.Prefs.resetBranch("");
     });
   });
 
   // Fake an unsuccessful login due to prolonged failure.
@@ -323,17 +323,17 @@ add_test(function test_newFailed_errorLo
     readFile(logfile, function(error, data) {
       do_check_true(Components.isSuccessCode(error));
       do_check_neq(data.indexOf(MESSAGE), -1);
 
       // Clean up.
       try {
         logfile.remove(false);
       } catch (ex) {
-        dump("Couldn't delete file: " + ex + "\n");
+        dump("Couldn't delete file: " + ex.message + "\n");
         // Stupid Windows box.
       }
 
       Svc.Prefs.resetBranch("");
 
     });
   });
   // newFailed is nonzero -- should write a log.
@@ -373,17 +373,17 @@ add_test(function test_errorLog_dumpAddo
     readFile(logfile, function(error, data) {
       do_check_true(Components.isSuccessCode(error));
       do_check_neq(data.indexOf("Addons installed"), -1);
 
       // Clean up.
       try {
         logfile.remove(false);
       } catch (ex) {
-        dump("Couldn't delete file: " + ex + "\n");
+        dump("Couldn't delete file: " + ex.message + "\n");
         // Stupid Windows box.
       }
 
       Svc.Prefs.resetBranch("");
     });
   });
 
   // Fake an unsuccessful sync due to prolonged failure.
@@ -425,17 +425,17 @@ add_test(function test_logErrorCleanup_a
       return e != logfile.leafName;
     }));
     do_check_false(entries.hasMoreElements());
 
     // Clean up.
     try {
       logfile.remove(false);
     } catch (ex) {
-      dump("Couldn't delete file: " + ex + "\n");
+      dump("Couldn't delete file: " + ex.message + "\n");
       // Stupid Windows box.
     }
 
     Svc.Prefs.resetBranch("");
     run_next_test();
   });
 
   let delay = CLEANUP_DELAY + DELAY_BUFFER;
--- a/services/sync/tests/unit/test_records_crypto.js
+++ b/services/sync/tests/unit/test_records_crypto.js
@@ -63,17 +63,17 @@ add_task(async function test_records_cry
 
     log.info("Make sure multiple decrypts cause failures");
     let error = "";
     try {
       payload = cryptoWrap.decrypt(keyBundle);
     } catch (ex) {
       error = ex;
     }
-    do_check_eq(error, "No ciphertext: nothing to decrypt?");
+    do_check_eq(error.message, "No ciphertext: nothing to decrypt?");
 
     log.info("Re-encrypting the record with alternate payload");
 
     cryptoWrap.cleartext.stuff = "another payload";
     cryptoWrap.encrypt(keyBundle);
     let secondIV = cryptoWrap.IV;
     payload = cryptoWrap.decrypt(keyBundle);
     do_check_eq(payload.stuff, "another payload");
@@ -85,28 +85,28 @@ add_task(async function test_records_cry
     cryptoWrap.encrypt(keyBundle);
     cryptoWrap.data.id = "other";
     error = "";
     try {
       cryptoWrap.decrypt(keyBundle);
     } catch (ex) {
       error = ex;
     }
-    do_check_eq(error, "Record id mismatch: resource != other");
+    do_check_eq(error.message, "Record id mismatch: resource != other");
 
     log.info("Make sure wrong hmacs cause failures");
     cryptoWrap.encrypt(keyBundle);
     cryptoWrap.hmac = "foo";
     error = "";
     try {
       cryptoWrap.decrypt(keyBundle);
     } catch (ex) {
       error = ex;
     }
-    do_check_eq(error.substr(0, 42), "Record SHA256 HMAC mismatch: should be foo");
+    do_check_eq(error.message.substr(0, 42), "Record SHA256 HMAC mismatch: should be foo");
 
     // Checking per-collection keys and default key handling.
 
     generateNewKeys(Service.collectionKeys);
     let bookmarkItem = prepareCryptoWrap("bookmarks", "foo");
     bookmarkItem.encrypt(Service.collectionKeys.keyForCollection("bookmarks"));
     log.info("Ciphertext is " + bookmarkItem.ciphertext);
     do_check_true(bookmarkItem.ciphertext != null);
@@ -128,17 +128,17 @@ add_task(async function test_records_cry
     // conceivably occur in the real world. Decryption will error, because
     // it's not the bookmarks key.
     let err;
     try {
       bookmarkItem.decrypt(Service.collectionKeys._default);
     } catch (ex) {
       err = ex;
     }
-    do_check_eq("Record SHA256 HMAC mismatch", err.substr(0, 27));
+    do_check_eq("Record SHA256 HMAC mismatch", err.message.substr(0, 27));
 
     // Explicitly check that it's using the bookmarks key.
     // This should succeed.
     do_check_eq(bookmarkItem.decrypt(Service.collectionKeys.keyForCollection("bookmarks")).stuff,
         "my payload here");
 
     do_check_true(Service.collectionKeys.hasKeysFor(["bookmarks"]));
 
--- a/services/sync/tests/unit/test_resource.js
+++ b/services/sync/tests/unit/test_resource.js
@@ -440,32 +440,32 @@ add_task(async function test() {
   do_check_eq(warnings.pop(), "${action} request to ${url} failed: ${ex}");
   do_check_eq(warnings.pop(),
               "Got exception calling onProgress handler during fetch of " +
               server.baseURI + "/json");
 
   // And this is what happens if JS throws an exception.
   res18 = new Resource(server.baseURI + "/json");
   onProgress = function(rec) {
-    throw "BOO!";
+    throw new Error("BOO!");
   };
   res18._onProgress = onProgress;
   let oldWarn = res18._log.warn;
   warnings = [];
   res18._log.warn = function(msg) { warnings.push(msg) };
   error = undefined;
   try {
     content = await res18.get();
   } catch (ex) {
     error = ex;
   }
 
   // It throws and logs.
-  do_check_eq(error.result, Cr.NS_ERROR_XPC_JS_THREW_STRING);
-  do_check_eq(error.message, "NS_ERROR_XPC_JS_THREW_STRING");
+  do_check_eq(error.result, Cr.NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS);
+  do_check_eq(error.message, "NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS");
   do_check_eq(warnings.pop(), "${action} request to ${url} failed: ${ex}");
   do_check_eq(warnings.pop(),
               "Got exception calling onProgress handler during fetch of " +
               server.baseURI + "/json");
 
   res18._log.warn = oldWarn;
 
   _("Ensure channel timeouts are thrown appropriately.");
--- a/services/sync/tests/unit/test_resource_async.js
+++ b/services/sync/tests/unit/test_resource_async.js
@@ -567,24 +567,24 @@ add_task(async function test_xpc_excepti
               "Got exception calling onProgress handler during fetch of " +
               server.baseURI + "/json");
 });
 
 add_task(async function test_js_exception_handling() {
   _("JS exception handling inside fetches.");
   let res15 = new AsyncResource(server.baseURI + "/json");
   res15._onProgress = function(rec) {
-    throw "BOO!";
+    throw new Error("BOO!");
   };
   let warnings = [];
   res15._log.warn = function(msg) { warnings.push(msg); };
 
   await Assert.rejects(res15.get(), error => {
-    do_check_eq(error.result, Cr.NS_ERROR_XPC_JS_THREW_STRING);
-    do_check_eq(error.message, "NS_ERROR_XPC_JS_THREW_STRING");
+    do_check_eq(error.result, Cr.NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS);
+    do_check_eq(error.message, "NS_ERROR_XPC_JAVASCRIPT_ERROR_WITH_DETAILS");
     return true;
   });
   do_check_eq(warnings.pop(),
               "${action} request to ${url} failed: ${ex}");
   do_check_eq(warnings.pop(),
               "Got exception calling onProgress handler during fetch of " +
               server.baseURI + "/json");
 });
--- a/services/sync/tests/unit/test_service_sync_locked.js
+++ b/services/sync/tests/unit/test_service_sync_locked.js
@@ -9,18 +9,24 @@ add_task(async function run_test() {
   let debug = [];
   let info  = [];
 
   function augmentLogger(old) {
     let d = old.debug;
     let i = old.info;
     // For the purposes of this test we don't need to do full formatting
     // of the 2nd param, as the ones we care about are always strings.
-    old.debug = function(m, p) { debug.push(p ? m + ": " + p : m); d.call(old, m, p); }
-    old.info  = function(m, p) { info.push(p ? m + ": " + p : m); i.call(old, m, p); }
+    old.debug = function(m, p) {
+      debug.push(p ? m + ": " + (p.message || p) : m);
+      d.call(old, m, p);
+    };
+    old.info = function(m, p) {
+      info.push(p ? m + ": " + (p.message || p) : m);
+      i.call(old, m, p);
+    };
     return old;
   }
 
   Log.repository.rootLogger.addAppender(new Log.DumpAppender());
 
   augmentLogger(Service._log);
 
   // Avoid daily ping
--- a/services/sync/tests/unit/test_syncengine_sync.js
+++ b/services/sync/tests/unit/test_syncengine_sync.js
@@ -968,24 +968,24 @@ add_task(async function test_processInco
                          "record-no-" + (2 + APPLY_BATCH_SIZE * 3),
                          "record-no-" + (1 + APPLY_BATCH_SIZE * 3)];
   let engine = makeRotaryEngine();
   engine.applyIncomingBatchSize = APPLY_BATCH_SIZE;
 
   engine.__reconcile = engine._reconcile;
   engine._reconcile = async function _reconcile(record) {
     if (BOGUS_RECORDS.indexOf(record.id) % 2 == 0) {
-      throw "I don't like this record! Baaaaaah!";
+      throw new Error("I don't like this record! Baaaaaah!");
     }
     return this.__reconcile.apply(this, arguments);
   };
   engine._store._applyIncoming = engine._store.applyIncoming;
   engine._store.applyIncoming = async function(record) {
     if (BOGUS_RECORDS.indexOf(record.id) % 2 == 1) {
-      throw "I don't like this record! Baaaaaah!";
+      throw new Error("I don't like this record! Baaaaaah!");
     }
     return this._applyIncoming.apply(this, arguments);
   };
 
   // Keep track of requests made of a collection.
   let count = 0;
   let uris  = [];
   function recording_handler(recordedCollection) {
@@ -1084,17 +1084,18 @@ add_task(async function test_processInco
                                   denomination: "Flying Scotsman"}));
   collection._wbos.nodecrypt = new ServerWBO("nodecrypt", "Decrypt this!");
   collection._wbos.nodecrypt2 = new ServerWBO("nodecrypt2", "Decrypt this!");
 
   // Patch the fake crypto service to throw on the record above.
   Weave.Crypto._decrypt = Weave.Crypto.decrypt;
   Weave.Crypto.decrypt = function(ciphertext) {
     if (ciphertext == "Decrypt this!") {
-      throw "Derp! Cipher finalized failed. Im ur crypto destroyin ur recordz.";
+      throw new Error(
+          "Derp! Cipher finalized failed. Im ur crypto destroyin ur recordz.");
     }
     return this._decrypt.apply(this, arguments);
   };
 
   // Some broken records also exist locally.
   let engine = makeRotaryEngine();
   engine.enabled = true;
   engine._store.items = {nojson: "Valid JSON",
@@ -1675,17 +1676,17 @@ add_task(async function test_sync_partia
   engine.lastSync = 123; // needs to be non-zero so that tracker is queried
   engine.lastSyncLocal = 456;
 
   // Let the third upload fail completely
   var noOfUploads = 0;
   collection.post = (function(orig) {
     return function() {
       if (noOfUploads == 2)
-        throw "FAIL!";
+        throw new Error("FAIL!");
       noOfUploads++;
       return orig.apply(this, arguments);
     };
   }(collection.post));
 
   // Create a bunch of records (and server side handlers)
   for (let i = 0; i < 234; i++) {
     let id = "record-no-" + i;
--- a/services/sync/tests/unit/test_telemetry.js
+++ b/services/sync/tests/unit/test_telemetry.js
@@ -113,17 +113,17 @@ add_task(async function test_processInco
   await SyncTestingInfrastructure(server);
   let collection = server.user("foo").collection("bookmarks");
   try {
     // Create a bogus record that when synced down will provoke a
     // network error which in turn provokes an exception in _processIncoming.
     const BOGUS_GUID = "zzzzzzzzzzzz";
     let bogus_record = collection.insert(BOGUS_GUID, "I'm a bogus record!");
     bogus_record.get = function get() {
-      throw "Sync this!";
+      throw new Error("Sync this!");
     };
     // Make the 10 minutes old so it will only be synced in the toFetch phase.
     bogus_record.modified = Date.now() / 1000 - 60 * 10;
     engine.lastSync = Date.now() / 1000 - 60;
     engine.toFetch = [BOGUS_GUID];
 
     let error, pingPayload, fullPing;
     try {
@@ -289,17 +289,17 @@ add_task(async function test_sync_partia
     ok(!!ping);
     ok(!ping.failureReason);
     equal(ping.engines.length, 1);
     equal(ping.engines[0].name, "rotary");
     ok(!ping.engines[0].incoming);
     ok(!ping.engines[0].failureReason);
     deepEqual(ping.engines[0].outgoing, [{ sent: 234, failed: 2 }]);
 
-    collection.post = function() { throw "Failure"; }
+    collection.post = function() { throw new Error("Failure"); }
 
     engine._store.items["record-no-1000"] = "Record No. 1000";
     engine._tracker.addChangedID("record-no-1000", 1000);
     collection.insert("record-no-1000", 1000);
 
     engine.lastSync = 123;
     engine.lastSyncLocal = 456;
     ping = null;
--- a/services/sync/tests/unit/test_utils_catch.js
+++ b/services/sync/tests/unit/test_utils_catch.js
@@ -1,14 +1,14 @@
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/service.js");
 
 add_task(async function run_test() {
   _("Make sure catch when copied to an object will correctly catch stuff");
-  let ret, rightThis, didCall, didThrow, wasTen, wasLocked;
+  let ret, rightThis, didCall, didThrow, wasCovfefe, wasLocked;
   let obj = {
     _catch: Utils.catch,
     _log: {
       debug(str) {
         didThrow = str.search(/^Exception/) == 0;
       },
       info(str) {
         wasLocked = str.indexOf("Cannot start sync: already syncing?") == 0;
@@ -22,92 +22,92 @@ add_task(async function run_test() {
         return 5;
       })();
     },
 
     throwy() {
       return this._catch(async function() {
         rightThis = this == obj;
         didCall = true;
-        throw 10;
+        throw new Error("covfefe");
       })();
     },
 
     callbacky() {
       return this._catch(async function() {
         rightThis = this == obj;
         didCall = true;
-        throw 10;
+        throw new Error("covfefe");
       }, async function(ex) {
-        wasTen = (ex == 10)
+        wasCovfefe = ex && ex.message == "covfefe";
       })();
     },
 
     lockedy() {
       return this._catch(async function() {
         rightThis = this == obj;
         didCall = true;
-        throw ("Could not acquire lock.");
+        Utils.throwLockException(null);
       })();
     },
 
     lockedy_chained() {
-      return this._catch(function() {
+      return this._catch(async function() {
         rightThis = this == obj;
         didCall = true;
-        return Promise.resolve().then( () => { throw ("Could not acquire lock.") });
+        Utils.throwLockException(null);
       })();
     },
   };
 
   _("Make sure a normal call will call and return");
   rightThis = didCall = didThrow = wasLocked = false;
   ret = await obj.func();
   do_check_eq(ret, 5);
   do_check_true(rightThis);
   do_check_true(didCall);
   do_check_false(didThrow);
-  do_check_eq(wasTen, undefined);
+  do_check_eq(wasCovfefe, undefined);
   do_check_false(wasLocked);
 
   _("Make sure catch/throw results in debug call and caller doesn't need to handle exception");
   rightThis = didCall = didThrow = wasLocked = false;
   ret = await obj.throwy();
   do_check_eq(ret, undefined);
   do_check_true(rightThis);
   do_check_true(didCall);
   do_check_true(didThrow);
-  do_check_eq(wasTen, undefined);
+  do_check_eq(wasCovfefe, undefined);
   do_check_false(wasLocked);
 
   _("Test callback for exception testing.");
   rightThis = didCall = didThrow = wasLocked = false;
   ret = await obj.callbacky();
   do_check_eq(ret, undefined);
   do_check_true(rightThis);
   do_check_true(didCall);
   do_check_true(didThrow);
-  do_check_true(wasTen);
+  do_check_true(wasCovfefe);
   do_check_false(wasLocked);
 
   _("Test the lock-aware catch that Service uses.");
   obj._catch = Service._catch;
   rightThis = didCall = didThrow = wasLocked = false;
-  wasTen = undefined;
+  wasCovfefe = undefined;
   ret = await obj.lockedy();
   do_check_eq(ret, undefined);
   do_check_true(rightThis);
   do_check_true(didCall);
   do_check_true(didThrow);
-  do_check_eq(wasTen, undefined);
+  do_check_eq(wasCovfefe, undefined);
   do_check_true(wasLocked);
 
   _("Test the lock-aware catch that Service uses with a chained promise.");
   rightThis = didCall = didThrow = wasLocked = false;
-  wasTen = undefined;
+  wasCovfefe = undefined;
   ret = await obj.lockedy_chained();
   do_check_eq(ret, undefined);
   do_check_true(rightThis);
   do_check_true(didCall);
   do_check_true(didThrow);
-  do_check_eq(wasTen, undefined);
+  do_check_eq(wasCovfefe, undefined);
   do_check_true(wasLocked);
 });
--- a/services/sync/tests/unit/test_utils_lock.js
+++ b/services/sync/tests/unit/test_utils_lock.js
@@ -60,17 +60,17 @@ add_task(async function run_test() {
   _("Make sure code that calls locked code throws");
   ret = null;
   rightThis = didCall = false;
   try {
     ret = await obj.throwy();
     do_throw("throwy internal call should have thrown!");
   } catch (ex) {
     // Should throw an Error, not a string.
-    do_check_begins(ex, "Could not acquire lock");
+    do_check_begins(ex.message, "Could not acquire lock");
   }
   do_check_eq(ret, null);
   do_check_true(rightThis);
   do_check_true(didCall);
   _("Lock should be called twice so state 3 is skipped");
   do_check_eq(lockState, 4);
   do_check_eq(lockedState, 5);
   do_check_eq(unlockState, 6);
--- a/services/sync/tests/unit/test_utils_notify.js
+++ b/services/sync/tests/unit/test_utils_notify.js
@@ -16,17 +16,17 @@ add_task(async function run_test() {
         return 5;
       })();
     },
 
     throwy() {
       return this.notify("bad", "one", async function() {
         rightThis = this == obj;
         didCall = true;
-        throw 10;
+        throw new Error("covfefe");
       })();
     }
   };
 
   let state = 0;
   let makeObs = function(topic) {
     let obj2 = {
       observe(subject, obsTopic, data) {
@@ -71,29 +71,29 @@ add_task(async function run_test() {
   rightThis = didCall = false;
   let ts = makeObs("foo:bad:start");
   let tf = makeObs("foo:bad:finish");
   let te = makeObs("foo:bad:error");
   try {
     ret = await obj.throwy();
     do_throw("throwy should have thrown!");
   } catch (ex) {
-    do_check_eq(ex, 10);
+    do_check_eq(ex.message, "covfefe");
   }
   do_check_eq(ret, null);
   do_check_true(rightThis);
   do_check_true(didCall);
 
   do_check_eq(ts.state, 3);
   do_check_eq(ts.subject, undefined);
   do_check_eq(ts.topic, "foo:bad:start");
   do_check_eq(ts.data, "one");
 
   do_check_eq(tf.state, undefined);
   do_check_eq(tf.subject, undefined);
   do_check_eq(tf.topic, undefined);
   do_check_eq(tf.data, undefined);
 
   do_check_eq(te.state, 4);
-  do_check_eq(te.subject, 10);
+  do_check_eq(te.subject.message, "covfefe");
   do_check_eq(te.topic, "foo:bad:error");
   do_check_eq(te.data, "one");
 });
--- a/services/sync/tps/extensions/mozmill/resource/stdlib/EventUtils.js
+++ b/services/sync/tps/extensions/mozmill/resource/stdlib/EventUtils.js
@@ -477,17 +477,17 @@ function _computeKeyCodeFromChar(aChar)
  * name begins with "VK_", or a character.
  */
 function isKeypressFiredKey(aDOMKeyCode)
 {
   if (typeof(aDOMKeyCode) == "string") {
     if (aDOMKeyCode.indexOf("VK_") == 0) {
       aDOMKeyCode = KeyEvent["DOM_" + aDOMKeyCode];
       if (!aDOMKeyCode) {
-        throw "Unknown key: " + aDOMKeyCode;
+        throw new Error(`Unknown key: ${aDOMKeyCode}`);
       }
     } else {
       // If the key generates a character, it must cause a keypress event.
       return true;
     }
   }
   switch (aDOMKeyCode) {
     case KeyEvent.DOM_VK_SHIFT:
@@ -524,17 +524,17 @@ function isKeypressFiredKey(aDOMKeyCode)
 function synthesizeKey(aKey, aEvent, aWindow)
 {
   var utils = _getDOMWindowUtils(aWindow);
   if (utils) {
     var keyCode = 0, charCode = 0;
     if (aKey.indexOf("VK_") == 0) {
       keyCode = KeyEvent["DOM_" + aKey];
       if (!keyCode) {
-        throw "Unknown key: " + aKey;
+        throw new Error(`Unknown key: ${aKey}`);
       }
     } else {
       charCode = aKey.charCodeAt(0);
       keyCode = _computeKeyCodeFromChar(aKey.charAt(0));
     }
 
     var modifiers = _parseModifiers(aEvent);
     var flags = 0;
--- a/services/sync/tps/extensions/mozmill/resource/stdlib/httpd.js
+++ b/services/sync/tps/extensions/mozmill/resource/stdlib/httpd.js
@@ -1715,17 +1715,17 @@ RequestReader.prototype =
       throw HTTP_400;
     }
 
     // determine HTTP version
     try
     {
       metadata._httpVersion = new nsHttpVersion(match[1]);
       if (!metadata._httpVersion.atLeast(nsHttpVersion.HTTP_1_0))
-        throw "unsupported HTTP version";
+        throw new Error("unsupported HTTP version");
     }
     catch (e)
     {
       // we support HTTP/1.0 and HTTP/1.1 only
       throw HTTP_501;
     }
 
 
@@ -4866,27 +4866,27 @@ function htmlEscape(str)
  *   or without leading zeros
  * @throws
  *   if versionString does not specify a valid HTTP version number
  */
 function nsHttpVersion(versionString)
 {
   var matches = /^(\d+)\.(\d+)$/.exec(versionString);
   if (!matches)
-    throw "Not a valid HTTP version!";
+    throw new Error("Not a valid HTTP version!");
 
   /** The major version number of this, as a number. */
   this.major = parseInt(matches[1], 10);
 
   /** The minor version number of this, as a number. */
   this.minor = parseInt(matches[2], 10);
 
   if (isNaN(this.major) || isNaN(this.minor) ||
       this.major < 0    || this.minor < 0)
-    throw "Not a valid HTTP version!";
+    throw new Error("Not a valid HTTP version!");
 }
 nsHttpVersion.prototype =
 {
   /**
    * Returns the standard string representation of the HTTP version represented
    * by this (e.g., "1.1").
    */
   toString: function ()
--- a/services/sync/tps/extensions/tps/resource/modules/history.jsm
+++ b/services/sync/tps/extensions/tps/resource/modules/history.jsm
@@ -181,16 +181,16 @@ var HistoryEntry = {
     } else if ("begin" in item && "end" in item) {
       let cb = Async.makeSpinningCallback();
       let msSinceEpoch = parseInt(usSinceEpoch / 1000);
       let filter = {
         beginDate: new Date(msSinceEpoch + (item.begin * 60 * 60 * 1000)),
         endDate: new Date(msSinceEpoch + (item.end * 60 * 60 * 1000))
       };
       PlacesUtils.history.removeVisitsByFilter(filter)
-      .catch(ex => Logger.AssertTrue(false, "An error occurred while deleting history: " + ex))
+      .catch(ex => Logger.AssertTrue(false, "An error occurred while deleting history: " + ex.message))
       .then(result => { cb(null, result) }, err => { cb(err) });
       Async.waitForSyncCallback(cb);
     } else {
       Logger.AssertTrue(false, "invalid entry in delete history");
     }
   },
 };
--- a/services/sync/tps/extensions/tps/resource/quit.js
+++ b/services/sync/tps/extensions/tps/resource/quit.js
@@ -46,14 +46,14 @@ function goQuitApplication() {
     forceQuit = Components.interfaces.nsIAppShellService.eForceQuit;
   } else {
     throw new Error("goQuitApplication: no AppStartup/appShell");
   }
 
   try {
     appService.quit(forceQuit);
   } catch (ex) {
-    throw new Error("goQuitApplication: " + ex);
+    throw new Error(`goQuitApplication: ${ex.message}`);
   }
 
   return true;
 }
 
--- a/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync_crypto.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_storage_sync_crypto.js
@@ -25,24 +25,24 @@ async function throwsGen(constraint, f) 
   } catch (e) {
     threw = true;
     exception = e;
   }
 
   ok(threw, "did not throw an exception");
 
   const debuggingMessage = `got ${exception}, expected ${constraint}`;
-  let message = exception;
-  if (typeof exception === "object") {
-    message = exception.message;
-  }
 
   if (typeof constraint === "function") {
-    ok(constraint(message), debuggingMessage);
+    ok(constraint(exception), debuggingMessage);
   } else {
+    let message = exception;
+    if (typeof exception === "object") {
+      message = exception.message;
+    }
     ok(constraint === message, debuggingMessage);
   }
 }
 
 /**
  * An EncryptionRemoteTransformer that uses a fixed key bundle,
  * suitable for testing.
  */