Bug 1375212 - Wrap thrown strings in Error objects draft
authorNicolas Ouellet-Payeur <nicolaso@google.com>
Sat, 22 Jul 2017 18:55:43 -0700
changeset 616092 6e7c7ef476faf18d7db92405e9d6a935fd813d66
parent 615935 388d81ed93fa640f91d155f36254667c734157cf
child 639368 1d1e71720aeacc0415285cd8e9a6551ffec20ea4
push id70574
push userbmo:nicolaso@google.com
push dateWed, 26 Jul 2017 17:54:37 +0000
bugs1375212
milestone56.0a1
Bug 1375212 - Wrap thrown strings in Error objects 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.
  */