Bug 1363581 - (part 2) Make RESTRequest's public API use promises and not callbacks r?markh draft
authorThom Chiovoloni <tchiovoloni@mozilla.com>
Wed, 14 Mar 2018 20:34:50 -0700
changeset 769464 30cf9c848eabaeba537009f4b25886263c1593eb
parent 769463 5ae2d805cc2783e08f918b9c4122661ce4706288
push id103133
push userbmo:tchiovoloni@mozilla.com
push dateMon, 19 Mar 2018 16:51:38 +0000
reviewersmarkh
bugs1363581
milestone61.0a1
Bug 1363581 - (part 2) Make RESTRequest's public API use promises and not callbacks r?markh This also took the opportunity to clean up and modernize code it touched (mostly tests, which needed changes but not quite as many changes as I ended up making). MozReview-Commit-ID: ApPUTHXFprM
services/common/hawkclient.js
services/common/hawkrequest.js
services/common/rest.js
services/common/tests/unit/test_hawkclient.js
services/common/tests/unit/test_hawkrequest.js
services/common/tests/unit/test_restrequest.js
services/common/tests/unit/test_tokenauthenticatedrequest.js
services/common/tokenserverclient.js
services/fxaccounts/FxAccountsConfig.jsm
services/fxaccounts/FxAccountsOAuthGrantClient.jsm
services/fxaccounts/FxAccountsProfileClient.jsm
services/fxaccounts/tests/xpcshell/test_oauth_grant_client.js
services/fxaccounts/tests/xpcshell/test_profile_client.js
services/sync/modules-testing/fxa_utils.js
services/sync/tests/unit/test_browserid_identity.js
services/sync/tests/unit/test_fxa_node_reassignment.js
services/sync/tests/unit/test_httpd_sync_server.js
services/sync/tests/unit/test_node_reassignment.js
--- a/services/common/hawkclient.js
+++ b/services/common/hawkclient.js
@@ -94,21 +94,16 @@ var HawkClient = function(host) {
   // Clock offset in milliseconds between our client's clock and the date
   // reported in responses from our host.
   this._localtimeOffsetMsec = 0;
 };
 
 this.HawkClient.prototype = {
 
   /*
-   * A boolean for feature detection.
-   */
-  willUTF8EncodeRequests: HAWKAuthenticatedRESTRequest.prototype.willUTF8EncodeObjectRequests,
-
-  /*
    * Construct an error message for a response.  Private.
    *
    * @param restResponse
    *        A RESTResponse object from a RESTRequest
    *
    * @param error
    *        A string or object describing the error
    */
@@ -196,123 +191,94 @@ this.HawkClient.prototype = {
    * @param extraHeaders
    *        An object with header/value pairs to send with the request.
    * @return Promise
    *        Returns a promise that resolves to the response of the API call,
    *        or is rejected with an error.  If the server response can be parsed
    *        as JSON and contains an 'error' property, the promise will be
    *        rejected with this JSON-parsed response.
    */
-  request(path, method, credentials = null, payloadObj = {}, extraHeaders = {},
-                    retryOK = true) {
+  async request(path, method, credentials = null, payloadObj = {}, extraHeaders = {},
+                retryOK = true) {
     method = method.toLowerCase();
 
-    let deferred = PromiseUtils.defer();
     let uri = this.host + path;
-    let self = this;
-
-    function _onComplete(error) {
-      // |error| can be either a normal caught error or an explicitly created
-      // Components.Exception() error. Log it now as it might not end up
-      // correctly in the logs by the time it's passed through _constructError.
-      if (error) {
-        log.warn("hawk request error", error);
-      }
-      // If there's no response there's nothing else to do.
-      if (!this.response) {
-        deferred.reject(error);
-        return;
-      }
-      let restResponse = this.response;
-      let status = restResponse.status;
-
-      log.debug("(Response) " + path + ": code: " + status +
-                " - Status text: " + restResponse.statusText);
-      if (logPII) {
-        log.debug("Response text: " + restResponse.body);
-      }
-
-      // All responses may have backoff headers, which are a server-side safety
-      // valve to allow slowing down clients without hurting performance.
-      self._maybeNotifyBackoff(restResponse, "x-weave-backoff");
-      self._maybeNotifyBackoff(restResponse, "x-backoff");
-
-      if (error) {
-        // When things really blow up, reconstruct an error object that follows
-        // the general format of the server on error responses.
-        deferred.reject(self._constructError(restResponse, error));
-        return;
-      }
-
-      self._updateClockOffset(restResponse.headers.date);
-
-      if (status === 401 && retryOK && !("retry-after" in restResponse.headers)) {
-        // Retry once if we were rejected due to a bad timestamp.
-        // Clock offset is adjusted already in the top of this function.
-        log.debug("Received 401 for " + path + ": retrying");
-        deferred.resolve(self.request(path, method, credentials, payloadObj, extraHeaders, false));
-        return;
-      }
-
-      // If the server returned a json error message, use it in the rejection
-      // of the promise.
-      //
-      // In the case of a 401, in which we are probably being rejected for a
-      // bad timestamp, retry exactly once, during which time clock offset will
-      // be adjusted.
-
-      let jsonResponse = {};
-      try {
-        jsonResponse = JSON.parse(restResponse.body);
-      } catch (notJSON) {}
-
-      let okResponse = (200 <= status && status < 300);
-      if (!okResponse || jsonResponse.error) {
-        if (jsonResponse.error) {
-          deferred.reject(jsonResponse);
-        } else {
-          deferred.reject(self._constructError(restResponse, "Request failed"));
-        }
-        return;
-      }
-      // It's up to the caller to know how to decode the response.
-      // We just return the whole response.
-      deferred.resolve(this.response);
-    }
-
-    function onComplete(error) {
-      try {
-        // |this| is the RESTRequest object and we need to ensure _onComplete
-        // gets the same one.
-        _onComplete.call(this, error);
-      } catch (ex) {
-        log.error("Unhandled exception processing response", ex);
-        deferred.reject(ex);
-      }
-    }
 
     let extra = {
       now: this.now(),
       localtimeOffsetMsec: this.localtimeOffsetMsec,
       headers: extraHeaders
     };
 
     let request = this.newHAWKAuthenticatedRESTRequest(uri, credentials, extra);
-    try {
-      if (method == "post" || method == "put" || method == "patch") {
-        request[method](payloadObj, onComplete);
-      } else {
-        request[method](onComplete);
-      }
-    } catch (ex) {
-      log.error("Failed to make hawk request", ex);
-      deferred.reject(ex);
+    let error;
+    let restResponse = await request[method](payloadObj).catch(e => {
+      // Keep a reference to the error, log a message about it, and return the
+      // response anyway.
+      error = e;
+      log.warn("hawk request error", error);
+      return request.response;
+    });
+
+    // This shouldn't happen anymore, but it's not exactly difficult to handle.
+    if (!restResponse) {
+      throw error;
+    }
+
+    let status = restResponse.status;
+
+    log.debug("(Response) " + path + ": code: " + status +
+              " - Status text: " + restResponse.statusText);
+    if (logPII) {
+      log.debug("Response text", restResponse.body);
+    }
+
+    // All responses may have backoff headers, which are a server-side safety
+    // valve to allow slowing down clients without hurting performance.
+    this._maybeNotifyBackoff(restResponse, "x-weave-backoff");
+    this._maybeNotifyBackoff(restResponse, "x-backoff");
+
+    if (error) {
+      // When things really blow up, reconstruct an error object that follows
+      // the general format of the server on error responses.
+      throw this._constructError(restResponse, error);
     }
 
-    return deferred.promise;
+    this._updateClockOffset(restResponse.headers.date);
+
+    if (status === 401 && retryOK && !("retry-after" in restResponse.headers)) {
+      // Retry once if we were rejected due to a bad timestamp.
+      // Clock offset is adjusted already in the top of this function.
+      log.debug("Received 401 for " + path + ": retrying");
+      return this.request(path, method, credentials, payloadObj, extraHeaders, false);
+    }
+
+    // If the server returned a json error message, use it in the rejection
+    // of the promise.
+    //
+    // In the case of a 401, in which we are probably being rejected for a
+    // bad timestamp, retry exactly once, during which time clock offset will
+    // be adjusted.
+
+    let jsonResponse = {};
+    try {
+      jsonResponse = JSON.parse(restResponse.body);
+    } catch (notJSON) {}
+
+    let okResponse = (200 <= status && status < 300);
+    if (!okResponse || jsonResponse.error) {
+      if (jsonResponse.error) {
+        throw jsonResponse;
+      }
+      throw this._constructError(restResponse, "Request failed");
+    }
+
+    // It's up to the caller to know how to decode the response.
+    // We just return the whole response.
+    return restResponse;
   },
 
   /*
    * The prefix used for all notifications sent by this module.  This
    * allows the handler of notifications to be sure they are handling
    * notifications for the service they expect.
    *
    * If not set, no notifications will be sent.
--- a/services/common/hawkrequest.js
+++ b/services/common/hawkrequest.js
@@ -61,17 +61,17 @@ var HAWKAuthenticatedRESTRequest =
   this.extraHeaders = extra.headers || {};
 
   // Expose for testing
   this._intl = getIntl();
 };
 HAWKAuthenticatedRESTRequest.prototype = {
   __proto__: RESTRequest.prototype,
 
-  dispatch: function dispatch(method, data, onComplete, onProgress) {
+  async dispatch(method, data) {
     let contentType = "text/plain";
     if (method == "POST" || method == "PUT" || method == "PATCH") {
       contentType = "application/json";
     }
     if (this.credentials) {
       let options = {
         now: this.now,
         localtimeOffsetMsec: this.localtimeOffsetMsec,
@@ -87,19 +87,17 @@ HAWKAuthenticatedRESTRequest.prototype =
     for (let header in this.extraHeaders) {
       this.setHeader(header, this.extraHeaders[header]);
     }
 
     this.setHeader("Content-Type", contentType);
 
     this.setHeader("Accept-Language", this._intl.accept_languages);
 
-    return RESTRequest.prototype.dispatch.call(
-      this, method, data, onComplete, onProgress
-    );
+    return super.dispatch(method, data);
   }
 };
 
 
 /**
   * Generic function to derive Hawk credentials.
   *
   * Hawk credentials are derived using shared secrets, which depend on the token
--- a/services/common/rest.js
+++ b/services/common/rest.js
@@ -8,16 +8,17 @@ var EXPORTED_SYMBOLS = [
   "TokenAuthenticatedRESTRequest",
 ];
 
 ChromeUtils.import("resource://gre/modules/Preferences.jsm");
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 ChromeUtils.import("resource://gre/modules/Log.jsm");
+ChromeUtils.import("resource://gre/modules/PromiseUtils.jsm");
 ChromeUtils.import("resource://services-common/utils.js");
 
 ChromeUtils.defineModuleGetter(this, "CryptoUtils",
                                "resource://services-crypto/utils.js");
 
 function decodeString(data, charset) {
   if (!data || !charset) {
     return data;
@@ -44,106 +45,69 @@ function decodeString(data, charset) {
       break;
     }
     remaining -= num;
     body += str.value;
   }
   return body;
 }
 
-
 /**
  * Single use HTTP requests to RESTish resources.
  *
  * @param uri
  *        URI for the request. This can be an nsIURI object or a string
  *        that can be used to create one. An exception will be thrown if
  *        the string is not a valid URI.
  *
  * Examples:
  *
  * (1) Quick GET request:
  *
- *   new RESTRequest("http://server/rest/resource").get(function (error) {
- *     if (error) {
- *       // Deal with a network error.
- *       processNetworkErrorCode(error.result);
- *       return;
- *     }
- *     if (!this.response.success) {
- *       // Bail out if we're not getting an HTTP 2xx code.
- *       processHTTPError(this.response.status);
- *       return;
- *     }
- *     processData(this.response.body);
- *   });
+ *   let response = await new RESTRequest("http://server/rest/resource").get();
+ *   if (!response.success) {
+ *     // Bail out if we're not getting an HTTP 2xx code.
+ *     processHTTPError(response.status);
+ *     return;
+ *   }
+ *   processData(response.body);
  *
  * (2) Quick PUT request (non-string data is automatically JSONified)
  *
- *   new RESTRequest("http://server/rest/resource").put(data, function (error) {
- *     ...
- *   });
- *
- * (3) Streaming GET
- *
- *   let request = new RESTRequest("http://server/rest/resource");
- *   request.setHeader("Accept", "application/newlines");
- *   request.onComplete = function (error) {
- *     if (error) {
- *       // Deal with a network error.
- *       processNetworkErrorCode(error.result);
- *       return;
- *     }
- *     callbackAfterRequestHasCompleted()
- *   });
- *   request.onProgress = function () {
- *     if (!this.response.success) {
- *       // Bail out if we're not getting an HTTP 2xx code.
- *       return;
- *     }
- *     // Process body data and reset it so we don't process the same data twice.
- *     processIncrementalData(this.response.body);
- *     this.response.body = "";
- *   });
- *   request.get();
+ *   let response = await new RESTRequest("http://server/rest/resource").put(data);
  */
 function RESTRequest(uri) {
   this.status = this.NOT_SENT;
 
   // If we don't have an nsIURI object yet, make one. This will throw if
   // 'uri' isn't a valid URI string.
   if (!(uri instanceof Ci.nsIURI)) {
     uri = Services.io.newURI(uri);
   }
   this.uri = uri;
 
   this._headers = {};
+  this._deferred = PromiseUtils.defer();
   this._log = Log.repository.getLogger(this._logName);
   this._log.manageLevelFromPref("services.common.log.logger.rest.request");
 }
+
 RESTRequest.prototype = {
 
   _logName: "Services.Common.RESTRequest",
 
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsIBadCertListener2,
     Ci.nsIInterfaceRequestor,
     Ci.nsIChannelEventSink
   ]),
 
   /** Public API: **/
 
   /**
-   * A constant boolean that indicates whether this object will automatically
-   * utf-8 encode request bodies passed as an object. Used for feature detection
-   * so, eg, loop can use the same source code for old and new Firefox versions.
-   */
-  willUTF8EncodeObjectRequests: true,
-
-  /**
    * URI for the request (an nsIURI object).
    */
   uri: null,
 
   /**
    * HTTP method (e.g. "GET")
    */
   method: null,
@@ -194,151 +158,107 @@ RESTRequest.prototype = {
    * The encoding with which the response to this request must be treated.
    * If a charset parameter is available in the HTTP Content-Type header for
    * this response, that will always be used, and this value is ignored. We
    * default to UTF-8 because that is a reasonable default.
    */
   charset: "utf-8",
 
   /**
-   * Called when the request has been completed, including failures and
-   * timeouts.
-   *
-   * @param error
-   *        Error that occurred while making the request, null if there
-   *        was no error.
-   */
-  onComplete: function onComplete(error) {
-  },
-
-  /**
-   * Called whenever data is being received on the channel. If this throws an
-   * exception, the request is aborted and the exception is passed as the
-   * error to onComplete().
-   */
-  onProgress: function onProgress() {
-  },
-
-  /**
    * Set a request header.
    */
-  setHeader: function setHeader(name, value) {
+  setHeader(name, value) {
     this._headers[name.toLowerCase()] = value;
   },
 
   /**
    * Perform an HTTP GET.
    *
-   * @param onComplete
-   *        Short-circuit way to set the 'onComplete' method. Optional.
-   * @param onProgress
-   *        Short-circuit way to set the 'onProgress' method. Optional.
-   *
-   * @return the request object.
+   * @return Promise<RESTResponse>
    */
-  get: function get(onComplete, onProgress) {
-    return this.dispatch("GET", null, onComplete, onProgress);
+  async get() {
+    return this.dispatch("GET", null);
   },
 
   /**
    * Perform an HTTP PATCH.
    *
    * @param data
    *        Data to be used as the request body. If this isn't a string
    *        it will be JSONified automatically.
-   * @param onComplete
-   *        Short-circuit way to set the 'onComplete' method. Optional.
-   * @param onProgress
-   *        Short-circuit way to set the 'onProgress' method. Optional.
    *
-   * @return the request object.
+   * @return Promise<RESTResponse>
    */
-  patch: function patch(data, onComplete, onProgress) {
-    return this.dispatch("PATCH", data, onComplete, onProgress);
+  async patch(data) {
+    return this.dispatch("PATCH", data);
   },
 
   /**
    * Perform an HTTP PUT.
    *
    * @param data
    *        Data to be used as the request body. If this isn't a string
    *        it will be JSONified automatically.
-   * @param onComplete
-   *        Short-circuit way to set the 'onComplete' method. Optional.
-   * @param onProgress
-   *        Short-circuit way to set the 'onProgress' method. Optional.
    *
-   * @return the request object.
+   * @return Promise<RESTResponse>
    */
-  put: function put(data, onComplete, onProgress) {
-    return this.dispatch("PUT", data, onComplete, onProgress);
+  async put(data) {
+    return this.dispatch("PUT", data);
   },
 
   /**
    * Perform an HTTP POST.
    *
    * @param data
    *        Data to be used as the request body. If this isn't a string
    *        it will be JSONified automatically.
-   * @param onComplete
-   *        Short-circuit way to set the 'onComplete' method. Optional.
-   * @param onProgress
-   *        Short-circuit way to set the 'onProgress' method. Optional.
    *
-   * @return the request object.
+   * @return Promise<RESTResponse>
    */
-  post: function post(data, onComplete, onProgress) {
-    return this.dispatch("POST", data, onComplete, onProgress);
+  async post(data) {
+    return this.dispatch("POST", data);
   },
 
   /**
    * Perform an HTTP DELETE.
    *
-   * @param onComplete
-   *        Short-circuit way to set the 'onComplete' method. Optional.
-   * @param onProgress
-   *        Short-circuit way to set the 'onProgress' method. Optional.
-   *
-   * @return the request object.
+   * @return Promise<RESTResponse>
    */
-  delete: function delete_(onComplete, onProgress) {
-    return this.dispatch("DELETE", null, onComplete, onProgress);
+  async delete() {
+    return this.dispatch("DELETE", null);
   },
 
   /**
    * Abort an active request.
    */
-  abort: function abort() {
+  abort(rejectWithError = null) {
     if (this.status != this.SENT && this.status != this.IN_PROGRESS) {
       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();
     }
+    if (rejectWithError) {
+      this._deferred.reject(rejectWithError);
+    }
   },
 
   /** Implementation stuff **/
 
-  dispatch: function dispatch(method, data, onComplete, onProgress) {
+  async dispatch(method, data) {
     if (this.status != this.NOT_SENT) {
       throw new Error("Request has already been sent!");
     }
 
     this.method = method;
-    if (onComplete) {
-      this.onComplete = onComplete;
-    }
-    if (onProgress) {
-      this.onProgress = onProgress;
-    }
 
     // Create and initialize HTTP channel.
     let channel = NetUtil.newChannel({uri: this.uri, loadUsingSystemPrincipal: true})
                          .QueryInterface(Ci.nsIRequest)
                          .QueryInterface(Ci.nsIHttpChannel);
     this.channel = channel;
     channel.loadFlags |= this.loadFlags;
     channel.notificationCallbacks = this;
@@ -402,161 +322,143 @@ RESTRequest.prototype = {
     channel.contentCharset = this.charset;
 
     // Blast off!
     try {
       channel.asyncOpen2(this);
     } catch (ex) {
       // asyncOpen can throw in a bunch of cases -- e.g., a forbidden port.
       this._log.warn("Caught an error in asyncOpen", ex);
-      CommonUtils.nextTick(onComplete.bind(this, ex));
+      this._deferred.reject(ex);
     }
     this.status = this.SENT;
     this.delayTimeout();
-    return this;
+    return this._deferred.promise;
   },
 
   /**
    * Create or push back the abort timer that kills this request.
    */
-  delayTimeout: function delayTimeout() {
+  delayTimeout() {
     if (this.timeout) {
       CommonUtils.namedTimer(this.abortTimeout, this.timeout * 1000, this,
                              "timeoutTimer");
     }
   },
 
   /**
    * Abort the request based on a timeout.
    */
-  abortTimeout: function abortTimeout() {
-    this.abort();
-    let error = Components.Exception("Aborting due to channel inactivity.",
-                                     Cr.NS_ERROR_NET_TIMEOUT);
-    if (!this.onComplete) {
-      this._log.error("Unexpected error: onComplete not defined in " +
-                      "abortTimeout.");
-      return;
-    }
-    this.onComplete(error);
+  abortTimeout() {
+    this.abort(Components.Exception("Aborting due to channel inactivity.",
+                                    Cr.NS_ERROR_NET_TIMEOUT));
   },
 
   /** nsIStreamListener **/
 
-  onStartRequest: function onStartRequest(channel) {
+  onStartRequest(channel) {
     if (this.status == this.ABORTED) {
       this._log.trace("Not proceeding with onStartRequest, request was aborted.");
+      // We might have already rejected, but just in case.
+      this._deferred.reject(Components.Exception("Request aborted", Cr.NS_BINDING_ABORTED));
       return;
     }
 
     try {
       channel.QueryInterface(Ci.nsIHttpChannel);
     } catch (ex) {
       this._log.error("Unexpected error: channel is not a nsIHttpChannel!");
       this.status = this.ABORTED;
       channel.cancel(Cr.NS_BINDING_ABORTED);
+      this._deferred.reject(ex);
       return;
     }
 
     this.status = this.IN_PROGRESS;
 
     this._log.trace("onStartRequest: " + channel.requestMethod + " " +
                     channel.URI.spec);
 
     // Create a new response object.
     this.response = new RESTResponse(this);
 
     this.delayTimeout();
   },
 
-  onStopRequest: function onStopRequest(channel, context, statusCode) {
+  onStopRequest(channel, context, statusCode) {
     if (this.timeoutTimer) {
       // Clear the abort timer now that the channel is done.
       this.timeoutTimer.clear();
     }
 
     // We don't want to do anything for a request that's already been aborted.
     if (this.status == this.ABORTED) {
       this._log.trace("Not proceeding with onStopRequest, request was aborted.");
+      // We might not have already rejected if the user called reject() manually.
+      // If we have already rejected, then this is a no-op
+      this._deferred.reject(Components.Exception("Request aborted",
+                                                 Cr.NS_BINDING_ABORTED));
       return;
     }
 
     try {
       channel.QueryInterface(Ci.nsIHttpChannel);
     } catch (ex) {
       this._log.error("Unexpected error: channel not nsIHttpChannel!");
       this.status = this.ABORTED;
+      this._deferred.reject(ex);
       return;
     }
+
     this.status = this.COMPLETED;
 
+    try {
+      this.response.body = decodeString(this.response._rawBody, this.response.charset);
+      this.response._rawBody = null;
+    } catch (ex) {
+      this._log.warn(`Exception decoding response - ${this.method} ${channel.URI.spec}`, ex);
+      this._deferred.reject(ex);
+      return;
+    }
+
     let statusSuccess = Components.isSuccessCode(statusCode);
     let uri = channel && channel.URI && channel.URI.spec || "<unknown>";
     this._log.trace("Channel for " + channel.requestMethod + " " + uri +
                     " returned status code " + statusCode);
 
-    if (!this.onComplete) {
-      this._log.error("Unexpected error: onComplete not defined in " +
-                      "abortRequest.");
-      this.onProgress = null;
-      return;
-    }
-
-    try {
-      // Decode this.response._rawBody,
-      this.response.body = decodeString(this.response._rawBody, this.response.charset);
-      this.response._rawBody = null;
-      // Call the 'progress' callback a single time with all the data.
-      this.onProgress();
-    } catch (ex) {
-      this._log.warn(`Exception handling response - ${this.method} ${channel.URI.spec}`, ex);
-      this.status = this.ABORTED;
-      this.onComplete(ex);
-      this.onComplete = this.onProgress = null;
-      return;
-    }
-
     // Throw the failure code and stop execution.  Use Components.Exception()
     // instead of Error() so the exception is QI-able and can be passed across
     // XPCOM borders while preserving the status code.
     if (!statusSuccess) {
       let message = Components.Exception("", statusCode).name;
       let error = Components.Exception(message, statusCode);
       this._log.debug(this.method + " " + uri + " failed: " + statusCode + " - " + message);
-      this.onComplete(error);
-      this.onComplete = this.onProgress = null;
+      this._deferred.reject(error);
       return;
     }
 
     this._log.debug(this.method + " " + uri + " " + this.response.status);
 
     // Additionally give the full response body when Trace logging.
     if (this._log.level <= Log.Level.Trace) {
-      this._log.trace(this.method + " body: " + this.response.body);
+      this._log.trace(this.method + " body", this.response.body);
     }
 
     delete this._inputStream;
 
-    this.onComplete(null);
-    this.onComplete = this.onProgress = null;
+    this._deferred.resolve(this.response);
   },
 
-  onDataAvailable: function onDataAvailable(channel, cb, stream, off, count) {
+  onDataAvailable(channel, cb, stream, off, count) {
     // We get an nsIRequest, which doesn't have contentCharset.
     try {
       channel.QueryInterface(Ci.nsIHttpChannel);
     } catch (ex) {
       this._log.error("Unexpected error: channel not nsIHttpChannel!");
-      this.abort();
-
-      if (this.onComplete) {
-        this.onComplete(ex);
-      }
-
-      this.onComplete = this.onProgress = null;
+      this.abort(ex);
       return;
     }
 
     if (channel.contentCharset) {
       this.response.charset = channel.contentCharset;
     } else {
       this.response.charset = null;
     }
@@ -575,39 +477,38 @@ RESTRequest.prototype = {
   /** nsIInterfaceRequestor **/
 
   getInterface(aIID) {
     return this.QueryInterface(aIID);
   },
 
   /** nsIBadCertListener2 **/
 
-  notifyCertProblem: function notifyCertProblem(socketInfo, sslStatus, targetHost) {
+  notifyCertProblem(socketInfo, sslStatus, targetHost) {
     this._log.warn("Invalid HTTPS certificate encountered!");
     // Suppress invalid HTTPS certificate warnings in the UI.
     // (The request will still fail.)
     return true;
   },
 
   /**
    * Returns true if headers from the old channel should be
    * copied to the new channel. Invoked when a channel redirect
    * is in progress.
    */
-  shouldCopyOnRedirect: function shouldCopyOnRedirect(oldChannel, newChannel, flags) {
+  shouldCopyOnRedirect(oldChannel, newChannel, flags) {
     let isInternal = !!(flags & Ci.nsIChannelEventSink.REDIRECT_INTERNAL);
     let isSameURI  = newChannel.URI.equals(oldChannel.URI);
     this._log.debug("Channel redirect: " + oldChannel.URI.spec + ", " +
                     newChannel.URI.spec + ", internal = " + isInternal);
     return isInternal && isSameURI;
   },
 
   /** nsIChannelEventSink **/
-  asyncOnChannelRedirect:
-    function asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
+  asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
 
     let oldSpec = (oldChannel && oldChannel.URI) ? oldChannel.URI.spec : "<undefined>";
     let newSpec = (newChannel && newChannel.URI) ? newChannel.URI.spec : "<undefined>";
     this._log.debug("Channel redirect: " + oldSpec + ", " + newSpec + ", " + flags);
 
     try {
       newChannel.QueryInterface(Ci.nsIHttpChannel);
     } catch (ex) {
@@ -745,20 +646,18 @@ RESTResponse.prototype = {
 function TokenAuthenticatedRESTRequest(uri, authToken, extra) {
   RESTRequest.call(this, uri);
   this.authToken = authToken;
   this.extra = extra || {};
 }
 TokenAuthenticatedRESTRequest.prototype = {
   __proto__: RESTRequest.prototype,
 
-  dispatch: function dispatch(method, data, onComplete, onProgress) {
+  async dispatch(method, data) {
     let sig = CryptoUtils.computeHTTPMACSHA1(
       this.authToken.id, this.authToken.key, method, this.uri, this.extra
     );
 
     this.setHeader("Authorization", sig.getHeader());
 
-    return RESTRequest.prototype.dispatch.call(
-      this, method, data, onComplete, onProgress
-    );
+    return super.dispatch(method, data);
   },
 };
--- a/services/common/tests/unit/test_hawkclient.js
+++ b/services/common/tests/unit/test_hawkclient.js
@@ -80,26 +80,26 @@ async function check_authenticated_reque
   let response = await client.request("/foo", method, TEST_CREDS, {foo: "bar"});
   let result = JSON.parse(response.body);
 
   Assert.equal("bar", result.foo);
 
   await promiseStopServer(server);
 }
 
-add_task(function test_authenticated_post_request() {
-  check_authenticated_request("POST");
+add_task(async function test_authenticated_post_request() {
+  await check_authenticated_request("POST");
 });
 
-add_task(function test_authenticated_put_request() {
-  check_authenticated_request("PUT");
+add_task(async function test_authenticated_put_request() {
+  await check_authenticated_request("PUT");
 });
 
-add_task(function test_authenticated_patch_request() {
-  check_authenticated_request("PATCH");
+add_task(async function test_authenticated_patch_request() {
+  await check_authenticated_request("PATCH");
 });
 
 add_task(async function test_extra_headers() {
   let server = httpd_setup({"/foo": (request, response) => {
       Assert.ok(request.hasHeader("Authorization"));
       Assert.ok(request.hasHeader("myHeader"));
       Assert.equal(request.getHeader("myHeader"), "fake");
 
--- a/services/common/tests/unit/test_hawkrequest.js
+++ b/services/common/tests/unit/test_hawkrequest.js
@@ -1,16 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/Log.jsm");
 ChromeUtils.import("resource://services-common/utils.js");
 ChromeUtils.import("resource://services-common/hawkrequest.js");
+ChromeUtils.import("resource://services-common/async.js");
 
 // https://github.com/mozilla/fxa-auth-server/wiki/onepw-protocol#wiki-use-session-certificatesign-etc
 var SESSION_KEYS = {
   sessionToken: h("a0a1a2a3a4a5a6a7 a8a9aaabacadaeaf" +
                   "b0b1b2b3b4b5b6b7 b8b9babbbcbdbebf"),
 
   tokenID:      h("c0a29dcf46174973 da1378696e4c82ae" +
                   "10f723cf4f4d9f75 e39f4ae3851595ab"),
@@ -69,18 +70,17 @@ add_test(function test_intl_accept_langu
       // We've checked all the entries in languages[]. Cleanup and move on.
       info("Checked " + testCount + " languages. Removing checkLanguagePref as pref observer.");
       Services.prefs.removeObserver("intl.accept_languages", checkLanguagePref);
       run_next_test();
     });
   }
 });
 
-add_test(function test_hawk_authenticated_request() {
-  let onProgressCalled = false;
+add_task(async function test_hawk_authenticated_request() {
   let postData = {your: "data"};
 
   // An arbitrary date - Feb 2, 1971.  It ends in a bunch of zeroes to make our
   // computation with the hawk timestamp easier, since hawk throws away the
   // millisecond values.
   let then = 34329600000;
 
   let clockSkew = 120000;
@@ -117,48 +117,40 @@ add_test(function test_hawk_authenticate
       Assert.equal(lang, acceptLanguage);
 
       let message = "yay";
       response.setStatusLine(request.httpVersion, 200, "OK");
       response.bodyOutputStream.write(message, message.length);
     }
   });
 
-  function onProgress() {
-    onProgressCalled = true;
-  }
-
-  function onComplete(error) {
-    Assert.equal(200, this.response.status);
-    Assert.equal(this.response.body, "yay");
-    Assert.ok(onProgressCalled);
-
-    Services.prefs.resetUserPrefs();
-    let pref = Services.prefs.getComplexValue(
-      "intl.accept_languages", Ci.nsIPrefLocalizedString);
-    Assert.notEqual(acceptLanguage, pref.data);
-
-    server.stop(run_next_test);
-  }
-
   let url = server.baseURI + "/elysium";
   let extra = {
     now: localTime,
     localtimeOffsetMsec: timeOffset
   };
 
   let request = new HAWKAuthenticatedRESTRequest(url, credentials, extra);
 
   // Allow hawk._intl to respond to the language pref change
-  CommonUtils.nextTick(function() {
-    request.post(postData, onComplete, onProgress);
-  });
+  await Async.promiseYield();
+
+  await request.post(postData);
+  Assert.equal(200, request.response.status);
+  Assert.equal(request.response.body, "yay");
+
+  Services.prefs.resetUserPrefs();
+  let pref = Services.prefs.getComplexValue(
+    "intl.accept_languages", Ci.nsIPrefLocalizedString);
+  Assert.notEqual(acceptLanguage, pref.data);
+
+  await promiseStopServer(server);
 });
 
-add_test(function test_hawk_language_pref_changed() {
+add_task(async function test_hawk_language_pref_changed() {
   let languages = [
     "zu-NP",        // Nepalese dialect of Zulu
     "fa-CG",        // Congolese dialect of Farsi
   ];
 
   let credentials = {
     id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
     key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
@@ -180,39 +172,34 @@ add_test(function test_hawk_language_pre
   let url = server.baseURI + "/foo";
   let request;
 
   setLanguage(languages[0]);
 
   // A new request should create the stateful object for tracking the current
   // language.
   request = new HAWKAuthenticatedRESTRequest(url, credentials);
-  CommonUtils.nextTick(testFirstLanguage);
-
-  function testFirstLanguage() {
-    Assert.equal(languages[0], request._intl.accept_languages);
 
-    // Change the language pref ...
-    setLanguage(languages[1]);
-    CommonUtils.nextTick(testRequest);
-  }
+  // Wait for change to propagate
+  await Async.promiseYield();
+  Assert.equal(languages[0], request._intl.accept_languages);
+
+  // Change the language pref ...
+  setLanguage(languages[1]);
+
 
-  function testRequest() {
-    // Change of language pref should be picked up, which we can see on the
-    // server by inspecting the request headers.
-    request = new HAWKAuthenticatedRESTRequest(url, credentials);
-    request.post({}, function(error) {
-      Assert.equal(null, error);
-      Assert.equal(200, this.response.status);
+  await Async.promiseYield();
+
+  request = new HAWKAuthenticatedRESTRequest(url, credentials);
+  let response = await request.post({});
 
-      Services.prefs.resetUserPrefs();
+  Assert.equal(200, response.status);
+  Services.prefs.resetUserPrefs();
 
-      server.stop(run_next_test);
-    });
-  }
+  await promiseStopServer(server);
 });
 
 add_task(function test_deriveHawkCredentials() {
   let credentials = deriveHawkCredentials(
     SESSION_KEYS.sessionToken, "sessionToken");
 
   Assert.equal(credentials.algorithm, "sha256");
   Assert.equal(credentials.id, SESSION_KEYS.tokenID);
--- a/services/common/tests/unit/test_restrequest.js
+++ b/services/common/tests/unit/test_restrequest.js
@@ -45,17 +45,17 @@ add_test(function test_attributes() {
 
   run_next_test();
 });
 
 /**
  * Verify that a proxy auth redirect doesn't break us. This has to be the first
  * request made in the file!
  */
-add_test(function test_proxy_auth_redirect() {
+add_task(async function test_proxy_auth_redirect() {
   let pacFetched = false;
   function pacHandler(metadata, response) {
     pacFetched = true;
     let body = 'function FindProxyForURL(url, host) { return "DIRECT"; }';
     response.setStatusLine(metadata.httpVersion, 200, "OK");
     response.setHeader("Content-Type", "application/x-ns-proxy-autoconfig", false);
     response.bodyOutputStream.write(body, body.length);
   }
@@ -70,118 +70,99 @@ add_test(function test_proxy_auth_redire
 
   let server = httpd_setup({
     "/original": original,
     "/pac3":     pacHandler
   });
   PACSystemSettings.PACURI = server.baseURI + "/pac3";
   installFakePAC();
 
-  let res = new RESTRequest(server.baseURI + "/original");
-  res.get(function(error) {
-    Assert.ok(pacFetched);
-    Assert.ok(fetched);
-    Assert.ok(!error);
-    Assert.ok(this.response.success);
-    Assert.equal("TADA!", this.response.body);
-    uninstallFakePAC();
-    server.stop(run_next_test);
-  });
+  let req = new RESTRequest(server.baseURI + "/original");
+  await req.get();
+
+  Assert.ok(pacFetched);
+  Assert.ok(fetched);
+
+  Assert.ok(req.response.success);
+  Assert.equal("TADA!", req.response.body);
+  uninstallFakePAC();
+  await promiseStopServer(server);
 });
 
 /**
  * Ensure that failures that cause asyncOpen to throw
  * result in callbacks being invoked.
  * Bug 826086.
  */
-add_test(function test_forbidden_port() {
+add_task(async function test_forbidden_port() {
   let request = new RESTRequest("http://localhost:6000/");
-  request.get(function(error) {
-    if (!error) {
-      do_throw("Should have got an error.");
-    }
-    Assert.equal(error.result, Cr.NS_ERROR_PORT_ACCESS_NOT_ALLOWED);
-    run_next_test();
-  });
+
+  await Assert.rejects(request.get(), error =>
+    error.result == Cr.NS_ERROR_PORT_ACCESS_NOT_ALLOWED);
 });
 
 /**
  * Demonstrate API short-hand: create a request and dispatch it immediately.
  */
-add_test(function test_simple_get() {
+add_task(async function test_simple_get() {
   let handler = httpd_handler(200, "OK", "Huzzah!");
   let server = httpd_setup({"/resource": handler});
-
-  let request = new RESTRequest(server.baseURI + "/resource").get(function(error) {
-    Assert.equal(error, null);
+  let request = new RESTRequest(server.baseURI + "/resource");
+  let promiseResponse = request.get();
 
-    Assert.equal(this.status, this.COMPLETED);
-    Assert.ok(this.response.success);
-    Assert.equal(this.response.status, 200);
-    Assert.equal(this.response.body, "Huzzah!");
-
-    server.stop(run_next_test);
-  });
   Assert.equal(request.status, request.SENT);
   Assert.equal(request.method, "GET");
+
+  let response = await promiseResponse;
+  Assert.equal(response, request.response);
+
+  Assert.equal(request.status, request.COMPLETED);
+  Assert.ok(response.success);
+  Assert.equal(response.status, 200);
+  Assert.equal(response.body, "Huzzah!");
+  await promiseStopServer(server);
 });
 
 /**
  * Test HTTP GET with all bells and whistles.
  */
-add_test(function test_get() {
+add_task(async function test_get() {
   let handler = httpd_handler(200, "OK", "Huzzah!");
   let server = httpd_setup({"/resource": handler});
 
   let request = new RESTRequest(server.baseURI + "/resource");
   Assert.equal(request.status, request.NOT_SENT);
 
-  request.onProgress = request.onComplete = function() {
-    do_throw("This function should have been overwritten!");
-  };
-
-  let onProgress_called = false;
-  function onProgress() {
-    onProgress_called = true;
-    Assert.ok(this.response.body.length > 0);
-
-    Assert.ok(!!(this.channel.loadFlags & Ci.nsIRequest.LOAD_BYPASS_CACHE));
-    Assert.ok(!!(this.channel.loadFlags & Ci.nsIRequest.INHIBIT_CACHING));
-  }
-
-  function onComplete(error) {
-    Assert.equal(error, null);
+  let promiseResponse = request.get();
 
-    Assert.equal(this.status, this.COMPLETED);
-    Assert.ok(this.response.success);
-    Assert.equal(this.response.status, 200);
-    Assert.equal(this.response.body, "Huzzah!");
-    Assert.equal(handler.request.method, "GET");
-
-    Assert.ok(onProgress_called);
-    CommonUtils.nextTick(function() {
-      Assert.equal(request.onComplete, null);
-      Assert.equal(request.onProgress, null);
-      server.stop(run_next_test);
-    });
-  }
-
-  Assert.equal(request.get(onComplete, onProgress), request);
   Assert.equal(request.status, request.SENT);
   Assert.equal(request.method, "GET");
-  do_check_throws(function() {
-    request.get();
-  });
+
+  Assert.ok(!!(request.channel.loadFlags & Ci.nsIRequest.LOAD_BYPASS_CACHE));
+  Assert.ok(!!(request.channel.loadFlags & Ci.nsIRequest.INHIBIT_CACHING));
+
+  let response = await promiseResponse;
+
+  Assert.equal(response, request.response);
+  Assert.equal(request.status, request.COMPLETED);
+  Assert.ok(request.response.success);
+  Assert.equal(request.response.status, 200);
+  Assert.equal(request.response.body, "Huzzah!");
+  Assert.equal(handler.request.method, "GET");
+
+  await Assert.rejects(request.get(), /Request has already been sent/);
+
+  await promiseStopServer(server);
 });
 
 /**
  * Test HTTP GET with UTF-8 content, and custom Content-Type.
  */
-add_test(function test_get_utf8() {
-  let response = "Hello World or Καλημέρα κόσμε or こんにちは 世界";
+add_task(async function test_get_utf8() {
+  let response = "Hello World or Καλημέρα κόσμε or こんにちは 世界 😺";
 
   let contentType = "text/plain";
   let charset = true;
   let charsetSuffix = "; charset=UTF-8";
 
   let server = httpd_setup({"/resource": function(req, res) {
     res.setStatusLine(req.httpVersion, 200, "OK");
     res.setHeader("Content-Type", contentType + (charset ? charsetSuffix : ""));
@@ -190,78 +171,95 @@ add_test(function test_get_utf8() {
                     .createInstance(Ci.nsIConverterOutputStream);
     converter.init(res.bodyOutputStream, "UTF-8");
     converter.writeString(response);
     converter.close();
   }});
 
   // Check if charset in Content-Type is propertly interpreted.
   let request1 = new RESTRequest(server.baseURI + "/resource");
-  request1.get(function(error) {
-    Assert.equal(null, error);
+  await request1.get();
+
+  Assert.equal(request1.response.status, 200);
+  Assert.equal(request1.response.body, response);
+  Assert.equal(request1.response.headers["content-type"],
+               contentType + charsetSuffix);
 
-    Assert.equal(request1.response.status, 200);
-    Assert.equal(request1.response.body, response);
-    Assert.equal(request1.response.headers["content-type"],
-                 contentType + charsetSuffix);
+  // Check that we default to UTF-8 if Content-Type doesn't have a charset
+  charset = false;
+  let request2 = new RESTRequest(server.baseURI + "/resource");
+  await request2.get();
+  Assert.equal(request2.response.status, 200);
+  Assert.equal(request2.response.body, response);
+  Assert.equal(request2.response.headers["content-type"], contentType);
+  Assert.equal(request2.response.charset, "utf-8");
+
+  let request3 = new RESTRequest(server.baseURI + "/resource");
 
-    // Check that we default to UTF-8 if Content-Type doesn't have a charset.
-    charset = false;
-    let request2 = new RESTRequest(server.baseURI + "/resource");
-    request2.get(function(error2) {
-      Assert.equal(null, error2);
+  // With the test server we tend to get onDataAvailable in chunks of 8192 (in
+  // real network requests there doesn't appear to be any pattern to the size of
+  // the data `onDataAvailable` is called with), the smiling cat emoji encodes as
+  // 4 bytes, and so when utf8 encoded, the `"a" + "😺".repeat(2048)` will not be
+  // aligned onto a codepoint.
+  //
+  // Since 8192 isn't guaranteed and could easily change, the following string is
+  // a) very long, and b) misaligned on roughly 3/4 of the bytes, as a safety
+  // measure.
+  response = ("a" + "😺".repeat(2048)).repeat(10);
 
-      Assert.equal(request2.response.status, 200);
-      Assert.equal(request2.response.body, response);
-      Assert.equal(request2.response.headers["content-type"], contentType);
-      Assert.equal(request2.response.charset, "utf-8");
+  await request3.get();
+
+  Assert.equal(request3.response.status, 200);
 
-      server.stop(run_next_test);
-    });
-  });
+  // Make sure it came through ok, despite the misalignment.
+  Assert.equal(request3.response.body, response);
+
+  await promiseStopServer(server);
 });
 
 /**
  * Test HTTP POST data is encoded as UTF-8 by default.
  */
-add_test(function test_post_utf8() {
+add_task(async function test_post_utf8() {
   // We setup a handler that responds with exactly what it received.
   // Given we've already tested above that responses are correctly utf-8
   // decoded we can surmise that the correct response coming back means the
   // input must also have been encoded.
   let server = httpd_setup({"/echo": function(req, res) {
     res.setStatusLine(req.httpVersion, 200, "OK");
     res.setHeader("Content-Type", req.getHeader("content-type"));
     // Get the body as bytes and write them back without touching them
     let sis = Cc["@mozilla.org/scriptableinputstream;1"]
               .createInstance(Ci.nsIScriptableInputStream);
     sis.init(req.bodyInputStream);
     let body = sis.read(sis.available());
     sis.close();
     res.write(body);
   }});
 
-  let data = {copyright: "\xa9"}; // \xa9 is the copyright symbol
+  let data = {
+    copyright: "©",
+    // See the comment in test_get_utf8 about this string.
+    long: ("a" + "😺".repeat(2048)).repeat(10)
+  };
   let request1 = new RESTRequest(server.baseURI + "/echo");
-  request1.post(data, function(error) {
-    Assert.equal(null, error);
+  await request1.post(data);
 
-    Assert.equal(request1.response.status, 200);
-    deepEqual(JSON.parse(request1.response.body), data);
-    Assert.equal(request1.response.headers["content-type"],
-                 "application/json; charset=utf-8");
+  Assert.equal(request1.response.status, 200);
+  deepEqual(JSON.parse(request1.response.body), data);
+  Assert.equal(request1.response.headers["content-type"],
+               "application/json; charset=utf-8");
 
-    server.stop(run_next_test);
-  });
+  await promiseStopServer(server);
 });
 
 /**
  * Test more variations of charset handling.
  */
-add_test(function test_charsets() {
+add_task(async function test_charsets() {
   let response = "Hello World, I can't speak Russian";
 
   let contentType = "text/plain";
   let charset = true;
   let charsetSuffix = "; charset=us-ascii";
 
   let server = httpd_setup({"/resource": function(req, res) {
     res.setStatusLine(req.httpVersion, 200, "OK");
@@ -272,519 +270,432 @@ add_test(function test_charsets() {
     converter.init(res.bodyOutputStream, "us-ascii");
     converter.writeString(response);
     converter.close();
   }});
 
   // Check that provided charset overrides hint.
   let request1 = new RESTRequest(server.baseURI + "/resource");
   request1.charset = "not-a-charset";
-  request1.get(function(error) {
-    Assert.equal(null, error);
-
-    Assert.equal(request1.response.status, 200);
-    Assert.equal(request1.response.body, response);
-    Assert.equal(request1.response.headers["content-type"],
-                 contentType + charsetSuffix);
-    Assert.equal(request1.response.charset, "us-ascii");
+  await request1.get();
+  Assert.equal(request1.response.status, 200);
+  Assert.equal(request1.response.body, response);
+  Assert.equal(request1.response.headers["content-type"],
+               contentType + charsetSuffix);
+  Assert.equal(request1.response.charset, "us-ascii");
 
-    // Check that hint is used if Content-Type doesn't have a charset.
-    charset = false;
-    let request2 = new RESTRequest(server.baseURI + "/resource");
-    request2.charset = "us-ascii";
-    request2.get(function(error2) {
-      Assert.equal(null, error2);
+  // Check that hint is used if Content-Type doesn't have a charset.
+  charset = false;
+  let request2 = new RESTRequest(server.baseURI + "/resource");
+  request2.charset = "us-ascii";
+  await request2.get();
 
-      Assert.equal(request2.response.status, 200);
-      Assert.equal(request2.response.body, response);
-      Assert.equal(request2.response.headers["content-type"], contentType);
-      Assert.equal(request2.response.charset, "us-ascii");
+  Assert.equal(request2.response.status, 200);
+  Assert.equal(request2.response.body, response);
+  Assert.equal(request2.response.headers["content-type"], contentType);
+  Assert.equal(request2.response.charset, "us-ascii");
 
-      server.stop(run_next_test);
-    });
-  });
+  await promiseStopServer(server);
 });
 
 /**
  * Used for testing PATCH/PUT/POST methods.
  */
-function check_posting_data(method) {
+async function check_posting_data(method) {
   let funcName = method.toLowerCase();
   let handler = httpd_handler(200, "OK", "Got it!");
   let server = httpd_setup({"/resource": handler});
 
   let request = new RESTRequest(server.baseURI + "/resource");
   Assert.equal(request.status, request.NOT_SENT);
-
-  request.onProgress = request.onComplete = function() {
-    do_throw("This function should have been overwritten!");
-  };
-
-  let onProgress_called = false;
-  function onProgress() {
-    onProgress_called = true;
-    Assert.ok(this.response.body.length > 0);
-  }
-
-  function onComplete(error) {
-    Assert.equal(error, null);
-
-    Assert.equal(this.status, this.COMPLETED);
-    Assert.ok(this.response.success);
-    Assert.equal(this.response.status, 200);
-    Assert.equal(this.response.body, "Got it!");
-
-    Assert.equal(handler.request.method, method);
-    Assert.equal(handler.request.body, "Hullo?");
-    Assert.equal(handler.request.getHeader("Content-Type"), "text/plain");
-
-    Assert.ok(onProgress_called);
-    CommonUtils.nextTick(function() {
-      Assert.equal(request.onComplete, null);
-      Assert.equal(request.onProgress, null);
-      server.stop(run_next_test);
-    });
-  }
-
-  Assert.equal(request[funcName]("Hullo?", onComplete, onProgress), request);
+  let responsePromise = request[funcName]("Hullo?");
   Assert.equal(request.status, request.SENT);
   Assert.equal(request.method, method);
-  do_check_throws(function() {
-    request[funcName]("Hai!");
-  });
+
+  let response = await responsePromise;
+
+  Assert.equal(response, request.response);
+
+  Assert.equal(request.status, request.COMPLETED);
+  Assert.ok(request.response.success);
+  Assert.equal(request.response.status, 200);
+  Assert.equal(request.response.body, "Got it!");
+
+  Assert.equal(handler.request.method, method);
+  Assert.equal(handler.request.body, "Hullo?");
+  Assert.equal(handler.request.getHeader("Content-Type"), "text/plain");
+
+  await Assert.rejects(request[funcName]("Hai!"),
+                       /Request has already been sent/);
+
+  await promiseStopServer(server);
 }
 
 /**
  * Test HTTP PATCH with a simple string argument and default Content-Type.
  */
-add_test(function test_patch() {
-  check_posting_data("PATCH");
+add_task(async function test_patch() {
+  await check_posting_data("PATCH");
 });
 
 /**
  * Test HTTP PUT with a simple string argument and default Content-Type.
  */
-add_test(function test_put() {
-  check_posting_data("PUT");
+add_task(async function test_put() {
+  await check_posting_data("PUT");
 });
 
 /**
  * Test HTTP POST with a simple string argument and default Content-Type.
  */
-add_test(function test_post() {
-  check_posting_data("POST");
+add_task(async function test_post() {
+  await check_posting_data("POST");
 });
 
 /**
  * Test HTTP DELETE.
  */
-add_test(function test_delete() {
+add_task(async function test_delete() {
   let handler = httpd_handler(200, "OK", "Got it!");
   let server = httpd_setup({"/resource": handler});
 
   let request = new RESTRequest(server.baseURI + "/resource");
   Assert.equal(request.status, request.NOT_SENT);
-
-  request.onProgress = request.onComplete = function() {
-    do_throw("This function should have been overwritten!");
-  };
-
-  let onProgress_called = false;
-  function onProgress() {
-    onProgress_called = true;
-    Assert.ok(this.response.body.length > 0);
-  }
-
-  function onComplete(error) {
-    Assert.equal(error, null);
-
-    Assert.equal(this.status, this.COMPLETED);
-    Assert.ok(this.response.success);
-    Assert.equal(this.response.status, 200);
-    Assert.equal(this.response.body, "Got it!");
-    Assert.equal(handler.request.method, "DELETE");
-
-    Assert.ok(onProgress_called);
-    CommonUtils.nextTick(function() {
-      Assert.equal(request.onComplete, null);
-      Assert.equal(request.onProgress, null);
-      server.stop(run_next_test);
-    });
-  }
-
-  Assert.equal(request.delete(onComplete, onProgress), request);
+  let responsePromise = request.delete();
   Assert.equal(request.status, request.SENT);
   Assert.equal(request.method, "DELETE");
-  do_check_throws(function() {
-    request.delete();
-  });
+
+  let response = await responsePromise;
+  Assert.equal(response, request.response);
+
+  Assert.equal(request.status, request.COMPLETED);
+  Assert.ok(request.response.success);
+  Assert.equal(request.response.status, 200);
+  Assert.equal(request.response.body, "Got it!");
+  Assert.equal(handler.request.method, "DELETE");
+
+  await Assert.rejects(request.delete(), /Request has already been sent/);
+
+  await promiseStopServer(server);
 });
 
 /**
  * Test an HTTP response with a non-200 status code.
  */
-add_test(function test_get_404() {
+add_task(async function test_get_404() {
   let handler = httpd_handler(404, "Not Found", "Cannae find it!");
   let server = httpd_setup({"/resource": handler});
 
   let request = new RESTRequest(server.baseURI + "/resource");
-  request.get(function(error) {
-    Assert.equal(error, null);
+  await request.get();
 
-    Assert.equal(this.status, this.COMPLETED);
-    Assert.ok(!this.response.success);
-    Assert.equal(this.response.status, 404);
-    Assert.equal(this.response.body, "Cannae find it!");
+  Assert.equal(request.status, request.COMPLETED);
+  Assert.ok(!request.response.success);
+  Assert.equal(request.response.status, 404);
+  Assert.equal(request.response.body, "Cannae find it!");
 
-    server.stop(run_next_test);
-  });
+  await promiseStopServer(server);
 });
 
 /**
  * The 'data' argument to PUT, if not a string already, is automatically
  * stringified as JSON.
  */
-add_test(function test_put_json() {
+add_task(async function test_put_json() {
   let handler = httpd_handler(200, "OK");
   let server = httpd_setup({"/resource": handler});
 
   let sample_data = {
     some: "sample_data",
     injson: "format",
     number: 42
   };
   let request = new RESTRequest(server.baseURI + "/resource");
-  request.put(sample_data, function(error) {
-    Assert.equal(error, null);
+  await request.put(sample_data);
 
-    Assert.equal(this.status, this.COMPLETED);
-    Assert.ok(this.response.success);
-    Assert.equal(this.response.status, 200);
-    Assert.equal(this.response.body, "");
+  Assert.equal(request.status, request.COMPLETED);
+  Assert.ok(request.response.success);
+  Assert.equal(request.response.status, 200);
+  Assert.equal(request.response.body, "");
 
-    Assert.equal(handler.request.method, "PUT");
-    Assert.equal(handler.request.body, JSON.stringify(sample_data));
-    Assert.equal(handler.request.getHeader("Content-Type"), "application/json; charset=utf-8");
+  Assert.equal(handler.request.method, "PUT");
+  Assert.equal(handler.request.body, JSON.stringify(sample_data));
+  Assert.equal(handler.request.getHeader("Content-Type"), "application/json; charset=utf-8");
 
-    server.stop(run_next_test);
-  });
+  await promiseStopServer(server);
 });
 
 /**
  * The 'data' argument to POST, if not a string already, is automatically
  * stringified as JSON.
  */
-add_test(function test_post_json() {
+add_task(async function test_post_json() {
   let handler = httpd_handler(200, "OK");
   let server = httpd_setup({"/resource": handler});
 
   let sample_data = {
     some: "sample_data",
     injson: "format",
     number: 42
   };
   let request = new RESTRequest(server.baseURI + "/resource");
-  request.post(sample_data, function(error) {
-    Assert.equal(error, null);
+  await request.post(sample_data);
 
-    Assert.equal(this.status, this.COMPLETED);
-    Assert.ok(this.response.success);
-    Assert.equal(this.response.status, 200);
-    Assert.equal(this.response.body, "");
+  Assert.equal(request.status, request.COMPLETED);
+  Assert.ok(request.response.success);
+  Assert.equal(request.response.status, 200);
+  Assert.equal(request.response.body, "");
 
-    Assert.equal(handler.request.method, "POST");
-    Assert.equal(handler.request.body, JSON.stringify(sample_data));
-    Assert.equal(handler.request.getHeader("Content-Type"), "application/json; charset=utf-8");
+  Assert.equal(handler.request.method, "POST");
+  Assert.equal(handler.request.body, JSON.stringify(sample_data));
+  Assert.equal(handler.request.getHeader("Content-Type"), "application/json; charset=utf-8");
 
-    server.stop(run_next_test);
-  });
+  await promiseStopServer(server);
 });
 
 /**
  * The content-type will be text/plain without a charset if the 'data' argument
  * to POST is already a string.
  */
-add_test(function test_post_json() {
+add_task(async function test_post_json() {
   let handler = httpd_handler(200, "OK");
   let server = httpd_setup({"/resource": handler});
 
   let sample_data = "hello";
   let request = new RESTRequest(server.baseURI + "/resource");
-  request.post(sample_data, function(error) {
-    Assert.equal(error, null);
+  await request.post(sample_data);
+  Assert.equal(request.status, request.COMPLETED);
+  Assert.ok(request.response.success);
+  Assert.equal(request.response.status, 200);
+  Assert.equal(request.response.body, "");
 
-    Assert.equal(this.status, this.COMPLETED);
-    Assert.ok(this.response.success);
-    Assert.equal(this.response.status, 200);
-    Assert.equal(this.response.body, "");
+  Assert.equal(handler.request.method, "POST");
+  Assert.equal(handler.request.body, sample_data);
+  Assert.equal(handler.request.getHeader("Content-Type"), "text/plain");
 
-    Assert.equal(handler.request.method, "POST");
-    Assert.equal(handler.request.body, sample_data);
-    Assert.equal(handler.request.getHeader("Content-Type"), "text/plain");
-
-    server.stop(run_next_test);
-  });
+  await promiseStopServer(server);
 });
 
 /**
  * HTTP PUT with a custom Content-Type header.
  */
-add_test(function test_put_override_content_type() {
+add_task(async function test_put_override_content_type() {
   let handler = httpd_handler(200, "OK");
   let server = httpd_setup({"/resource": handler});
 
   let request = new RESTRequest(server.baseURI + "/resource");
   request.setHeader("Content-Type", "application/lolcat");
-  request.put("O HAI!!1!", function(error) {
-    Assert.equal(error, null);
+  await request.put("O HAI!!1!");
 
-    Assert.equal(this.status, this.COMPLETED);
-    Assert.ok(this.response.success);
-    Assert.equal(this.response.status, 200);
-    Assert.equal(this.response.body, "");
+  Assert.equal(request.status, request.COMPLETED);
+  Assert.ok(request.response.success);
+  Assert.equal(request.response.status, 200);
+  Assert.equal(request.response.body, "");
 
-    Assert.equal(handler.request.method, "PUT");
-    Assert.equal(handler.request.body, "O HAI!!1!");
-    Assert.equal(handler.request.getHeader("Content-Type"), "application/lolcat");
+  Assert.equal(handler.request.method, "PUT");
+  Assert.equal(handler.request.body, "O HAI!!1!");
+  Assert.equal(handler.request.getHeader("Content-Type"), "application/lolcat");
 
-    server.stop(run_next_test);
-  });
+  await promiseStopServer(server);
 });
 
 /**
  * HTTP POST with a custom Content-Type header.
  */
-add_test(function test_post_override_content_type() {
+add_task(async function test_post_override_content_type() {
   let handler = httpd_handler(200, "OK");
   let server = httpd_setup({"/resource": handler});
 
   let request = new RESTRequest(server.baseURI + "/resource");
   request.setHeader("Content-Type", "application/lolcat");
-  request.post("O HAI!!1!", function(error) {
-    Assert.equal(error, null);
+  await request.post("O HAI!!1!");
 
-    Assert.equal(this.status, this.COMPLETED);
-    Assert.ok(this.response.success);
-    Assert.equal(this.response.status, 200);
-    Assert.equal(this.response.body, "");
+  Assert.equal(request.status, request.COMPLETED);
+  Assert.ok(request.response.success);
+  Assert.equal(request.response.status, 200);
+  Assert.equal(request.response.body, "");
 
-    Assert.equal(handler.request.method, "POST");
-    Assert.equal(handler.request.body, "O HAI!!1!");
-    Assert.equal(handler.request.getHeader("Content-Type"), "application/lolcat");
+  Assert.equal(handler.request.method, "POST");
+  Assert.equal(handler.request.body, "O HAI!!1!");
+  Assert.equal(handler.request.getHeader("Content-Type"), "application/lolcat");
 
-    server.stop(run_next_test);
-  });
+  await promiseStopServer(server);
 });
 
 /**
  * No special headers are sent by default on a GET request.
  */
-add_test(function test_get_no_headers() {
+add_task(async function test_get_no_headers() {
   let handler = httpd_handler(200, "OK");
   let server = httpd_setup({"/resource": handler});
 
   let ignore_headers = ["host", "user-agent", "accept", "accept-language",
                         "accept-encoding", "accept-charset", "keep-alive",
                         "connection", "pragma", "cache-control",
                         "content-length"];
+  let request = new RESTRequest(server.baseURI + "/resource");
+  await request.get();
 
-  new RESTRequest(server.baseURI + "/resource").get(function(error) {
-    Assert.equal(error, null);
-
-    Assert.equal(this.response.status, 200);
-    Assert.equal(this.response.body, "");
+  Assert.equal(request.response.status, 200);
+  Assert.equal(request.response.body, "");
 
-    let server_headers = handler.request.headers;
-    while (server_headers.hasMoreElements()) {
-      let header = server_headers.getNext().toString();
-      if (!ignore_headers.includes(header)) {
-        do_throw("Got unexpected header!");
-      }
+  let server_headers = handler.request.headers;
+  while (server_headers.hasMoreElements()) {
+    let header = server_headers.getNext().toString();
+    if (!ignore_headers.includes(header)) {
+      do_throw("Got unexpected header!");
     }
+  }
 
-    server.stop(run_next_test);
-  });
+  await promiseStopServer(server);
 });
 
 /**
  * Test changing the URI after having created the request.
  */
-add_test(function test_changing_uri() {
+add_task(async function test_changing_uri() {
   let handler = httpd_handler(200, "OK");
   let server = httpd_setup({"/resource": handler});
 
   let request = new RESTRequest("http://localhost:1234/the-wrong-resource");
   request.uri = CommonUtils.makeURI(server.baseURI + "/resource");
-  request.get(function(error) {
-    Assert.equal(error, null);
-    Assert.equal(this.response.status, 200);
-    server.stop(run_next_test);
-  });
+  let response = await request.get();
+  Assert.equal(response.status, 200);
+  await promiseStopServer(server);
 });
 
 /**
  * Test setting HTTP request headers.
  */
-add_test(function test_request_setHeader() {
+add_task(async function test_request_setHeader() {
   let handler = httpd_handler(200, "OK");
   let server = httpd_setup({"/resource": handler});
 
   let request = new RESTRequest(server.baseURI + "/resource");
 
   request.setHeader("X-What-Is-Weave", "awesome");
   request.setHeader("X-WHAT-is-Weave", "more awesomer");
   request.setHeader("Another-Header", "Hello World");
+  await request.get();
 
-  request.get(function(error) {
-    Assert.equal(error, null);
+  Assert.equal(request.response.status, 200);
+  Assert.equal(request.response.body, "");
 
-    Assert.equal(this.response.status, 200);
-    Assert.equal(this.response.body, "");
+  Assert.equal(handler.request.getHeader("X-What-Is-Weave"), "more awesomer");
+  Assert.equal(handler.request.getHeader("another-header"), "Hello World");
 
-    Assert.equal(handler.request.getHeader("X-What-Is-Weave"), "more awesomer");
-    Assert.equal(handler.request.getHeader("another-header"), "Hello World");
-
-    server.stop(run_next_test);
-  });
+  await promiseStopServer(server);
 });
 
 /**
  * Test receiving HTTP response headers.
  */
-add_test(function test_response_headers() {
+add_task(async function test_response_headers() {
   function handler(request, response) {
     response.setHeader("X-What-Is-Weave", "awesome");
     response.setHeader("Another-Header", "Hello World");
     response.setStatusLine(request.httpVersion, 200, "OK");
   }
   let server = httpd_setup({"/resource": handler});
   let request = new RESTRequest(server.baseURI + "/resource");
+  await request.get();
 
-  request.get(function(error) {
-    Assert.equal(error, null);
+  Assert.equal(request.response.status, 200);
+  Assert.equal(request.response.body, "");
 
-    Assert.equal(this.response.status, 200);
-    Assert.equal(this.response.body, "");
+  Assert.equal(request.response.headers["x-what-is-weave"], "awesome");
+  Assert.equal(request.response.headers["another-header"], "Hello World");
 
-    Assert.equal(this.response.headers["x-what-is-weave"], "awesome");
-    Assert.equal(this.response.headers["another-header"], "Hello World");
-
-    server.stop(run_next_test);
-  });
+  await promiseStopServer(server);
 });
 
 /**
  * The onComplete() handler gets called in case of any network errors
  * (e.g. NS_ERROR_CONNECTION_REFUSED).
  */
-add_test(function test_connection_refused() {
+add_task(async function test_connection_refused() {
   let request = new RESTRequest("http://localhost:1234/resource");
-  request.onProgress = function onProgress() {
-    do_throw("Shouldn't have called request.onProgress()!");
-  };
-  request.get(function(error) {
-    Assert.equal(error.result, Cr.NS_ERROR_CONNECTION_REFUSED);
-    Assert.equal(error.message, "NS_ERROR_CONNECTION_REFUSED");
-    Assert.equal(this.status, this.COMPLETED);
-    run_next_test();
-  });
-  Assert.equal(request.status, request.SENT);
+
+  // Fail the test if we resolve, return the error if we reject
+  await Assert.rejects(request.get(), error =>
+    error.result == Cr.NS_ERROR_CONNECTION_REFUSED &&
+    error.message == "NS_ERROR_CONNECTION_REFUSED");
+
+  Assert.equal(request.status, request.COMPLETED);
 });
 
 /**
  * Abort a request that just sent off.
  */
-add_test(function test_abort() {
+add_task(async function test_abort() {
   function handler() {
     do_throw("Shouldn't have gotten here!");
   }
   let server = httpd_setup({"/resource": handler});
 
   let request = new RESTRequest(server.baseURI + "/resource");
 
   // Aborting a request that hasn't been sent yet is pointless and will throw.
   do_check_throws(function() {
     request.abort();
   });
 
-  request.onProgress = request.onComplete = function() {
-    do_throw("Shouldn't have gotten here!");
-  };
-  request.get();
+  let responsePromise = request.get();
   request.abort();
 
   // Aborting an already aborted request is pointless and will throw.
   do_check_throws(function() {
     request.abort();
   });
 
   Assert.equal(request.status, request.ABORTED);
-  CommonUtils.nextTick(function() {
-    server.stop(run_next_test);
-  });
+
+  await Assert.rejects(responsePromise);
+
+  await promiseStopServer(server);
 });
 
 /**
  * A non-zero 'timeout' property specifies the amount of seconds to wait after
  * channel activity until the request is automatically canceled.
  */
-add_test(function test_timeout() {
+add_task(async function test_timeout() {
   let server = new HttpServer();
   let server_connection;
   server._handler.handleResponse = function(connection) {
     // This is a handler that doesn't do anything, just keeps the connection
     // open, thereby mimicking a timing out connection. We keep a reference to
     // the open connection for later so it can be properly disposed of. That's
     // why you really only want to make one HTTP request to this server ever.
     server_connection = connection;
   };
   server.start();
   let identity = server.identity;
   let uri = identity.primaryScheme + "://" + identity.primaryHost + ":" +
             identity.primaryPort;
 
   let request = new RESTRequest(uri + "/resource");
   request.timeout = 0.1; // 100 milliseconds
-  request.get(function(error) {
-    Assert.equal(error.result, Cr.NS_ERROR_NET_TIMEOUT);
-    Assert.equal(this.status, this.ABORTED);
+
+  await Assert.rejects(request.get(), error =>
+    error.result == Cr.NS_ERROR_NET_TIMEOUT);
+
+  Assert.equal(request.status, request.ABORTED);
 
-    // server_connection is undefined on the Android emulator for reasons
-    // unknown. Yet, we still get here. If this test is refactored, we should
-    // investigate the reason why the above callback is behaving differently.
-    if (server_connection) {
-      _("Closing connection.");
-      server_connection.close();
-    }
-
-    _("Shutting down server.");
-    server.stop(run_next_test);
-  });
+  // server_connection is undefined on the Android emulator for reasons
+  // unknown. Yet, we still get here. If this test is refactored, we should
+  // investigate the reason why the above callback is behaving differently.
+  if (server_connection) {
+    _("Closing connection.");
+    server_connection.close();
+  }
+  await promiseStopServer(server);
 });
 
-/**
- * An exception thrown in 'onProgress' propagates to the 'onComplete' handler.
- */
-add_test(function test_exception_in_onProgress() {
-  let handler = httpd_handler(200, "OK", "Foobar");
-  let server = httpd_setup({"/resource": handler});
-
-  let request = new RESTRequest(server.baseURI + "/resource");
-  request.onProgress = function onProgress() {
-    it.does.not.exist(); // eslint-disable-line no-undef
-  };
-  request.get(function onComplete(error) {
-    Assert.equal(error, "ReferenceError: it is not defined");
-    Assert.equal(this.status, this.ABORTED);
-
-    server.stop(run_next_test);
-  });
-});
-
-add_test(function test_new_channel() {
+add_task(async function test_new_channel() {
   _("Ensure a redirect to a new channel is handled properly.");
 
   function checkUA(metadata) {
     let ua = metadata.getHeader("User-Agent");
     _("User-Agent is " + ua);
     Assert.equal("foo bar", ua);
   }
 
@@ -809,61 +720,54 @@ add_test(function test_new_channel() {
     response.setHeader("Content-Type", "text/plain");
     response.bodyOutputStream.write(body, body.length);
   }
 
   let server1 = httpd_setup({"/redirect": redirectHandler});
   let server2 = httpd_setup({"/resource": resourceHandler});
   redirectURL = server2.baseURI + "/resource";
 
-  function advance() {
-    server1.stop(function() {
-      server2.stop(run_next_test);
-    });
-  }
-
   let request = new RESTRequest(server1.baseURI + "/redirect");
   request.setHeader("User-Agent", "foo bar");
 
   // Swizzle in our own fakery, because this redirect is neither
   // internal nor URI-preserving. RESTRequest's policy is to only
   // copy headers under certain circumstances.
   let protoMethod = request.shouldCopyOnRedirect;
   request.shouldCopyOnRedirect = function wrapped(o, n, f) {
     // Check the default policy.
     Assert.ok(!protoMethod.call(this, o, n, f));
     return true;
   };
 
-  request.get(function onComplete(error) {
-    let response = this.response;
+  let response = await request.get();
 
-    Assert.equal(200, response.status);
-    Assert.equal("Test", response.body);
-    Assert.ok(redirectRequested);
-    Assert.ok(resourceRequested);
+  Assert.equal(200, response.status);
+  Assert.equal("Test", response.body);
+  Assert.ok(redirectRequested);
+  Assert.ok(resourceRequested);
 
-    advance();
-  });
+  await promiseStopServer(server1);
+  await promiseStopServer(server2);
 });
 
-add_test(function test_not_sending_cookie() {
+add_task(async function test_not_sending_cookie() {
   function handler(metadata, response) {
     let body = "COOKIE!";
     response.setStatusLine(metadata.httpVersion, 200, "OK");
     response.bodyOutputStream.write(body, body.length);
     Assert.ok(!metadata.hasHeader("Cookie"));
   }
   let server = httpd_setup({"/test": handler});
 
   let cookieSer = Cc["@mozilla.org/cookieService;1"]
                     .getService(Ci.nsICookieService);
   let uri = CommonUtils.makeURI(server.baseURI);
   cookieSer.setCookieString(uri, null, "test=test; path=/;", null);
 
   let res = new RESTRequest(server.baseURI + "/test");
-  res.get(function(error) {
-    Assert.equal(null, error);
-    Assert.ok(this.response.success);
-    Assert.equal("COOKIE!", this.response.body);
-    server.stop(run_next_test);
-  });
+  let response = await res.get();
+
+  Assert.ok(response.success);
+  Assert.equal("COOKIE!", response.body);
+
+  await promiseStopServer(server);
 });
--- a/services/common/tests/unit/test_tokenauthenticatedrequest.js
+++ b/services/common/tests/unit/test_tokenauthenticatedrequest.js
@@ -36,15 +36,14 @@ add_task(async function test_authenticat
       response.bodyOutputStream.write(message, message.length);
     }
   });
   let uri = CommonUtils.makeURI(server.baseURI + "/foo");
   let sig = CryptoUtils.computeHTTPMACSHA1(id, key, method, uri, extra);
   auth = sig.getHeader();
 
   let req = new TokenAuthenticatedRESTRequest(uri, {id, key}, extra);
-  let error = await new Promise(res => req.get(res));
+  await req.get();
 
-  Assert.equal(null, error);
   Assert.equal(message, req.response.body);
 
   await promiseStopServer(server);
 });
--- a/services/common/tokenserverclient.js
+++ b/services/common/tokenserverclient.js
@@ -233,24 +233,22 @@ TokenServerClient.prototype = {
 
     let req = this.newRESTRequest(url);
     req.setHeader("Accept", "application/json");
     req.setHeader("Authorization", "BrowserID " + assertion);
 
     for (let header in addHeaders) {
       req.setHeader(header, addHeaders[header]);
     }
-
-    let response = await new Promise((resolve, reject) => {
-      req.get(function(err) {
-        // Yes this is weird, the callback's |this| gets bound to the RESTRequest object.
-        err ? reject(new TokenServerClientNetworkError(err)) :
-              resolve(this.response);
-      });
-    });
+    let response;
+    try {
+      response = await req.get();
+    } catch (err) {
+      throw new TokenServerClientNetworkError(err);
+    }
 
     try {
       return this._processTokenResponse(response);
     } catch (ex) {
       if (ex instanceof TokenServerClientServerError) {
         throw ex;
       }
       this._log.warn("Error processing token server response", ex);
--- a/services/fxaccounts/FxAccountsConfig.jsm
+++ b/services/fxaccounts/FxAccountsConfig.jsm
@@ -172,41 +172,36 @@ var FxAccountsConfig = {
   // This is only done before sign-in and sign-up, and even then only if the
   // `identity.fxaccounts.autoconfig.uri` preference is set.
   async fetchConfigURLs() {
     let rootURL = this.getAutoConfigURL();
     if (!rootURL) {
       return;
     }
     let configURL = rootURL + "/.well-known/fxa-client-configuration";
-    let jsonStr = await new Promise((resolve, reject) => {
-      let request = new RESTRequest(configURL);
-      request.setHeader("Accept", "application/json");
-      request.get(error => {
-        if (error) {
-          log.error(`Failed to get configuration object from "${configURL}"`, error);
-          reject(error);
-          return;
-        }
-        if (!request.response.success) {
-          log.error(`Received HTTP response code ${request.response.status} from configuration object request`);
-          if (request.response && request.response.body) {
-            log.debug("Got error response", request.response.body);
-          }
-          reject(request.response.status);
-          return;
-        }
-        resolve(request.response.body);
-      });
+    let request = new RESTRequest(configURL);
+    request.setHeader("Accept", "application/json");
+
+    // Catch and rethrow the error inline.
+    let resp = await request.get().catch(e => {
+      log.error(`Failed to get configuration object from "${configURL}"`, e);
+      throw e;
     });
+    if (!resp.success) {
+      log.error(`Received HTTP response code ${resp.status} from configuration object request`);
+      if (resp.body) {
+        log.debug("Got error response", resp.body);
+      }
+      throw new Error(`HTTP status ${resp.status} from configuration object request`);
+    }
 
-    log.debug("Got successful configuration response", jsonStr);
+    log.debug("Got successful configuration response", resp.body);
     try {
       // Update the prefs directly specified by the config.
-      let config = JSON.parse(jsonStr);
+      let config = JSON.parse(resp.body);
       let authServerBase = config.auth_server_base_url;
       if (!authServerBase.endsWith("/v1")) {
         authServerBase += "/v1";
       }
       Services.prefs.setCharPref("identity.fxaccounts.auth.uri", authServerBase);
       Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", config.oauth_server_base_url + "/v1");
       Services.prefs.setCharPref("identity.fxaccounts.remote.profile.uri", config.profile_server_base_url + "/v1");
       Services.prefs.setCharPref("identity.sync.tokenserver.uri", config.sync_tokenserver_base_url + "/1.0/sync/1.5");
--- a/services/fxaccounts/FxAccountsOAuthGrantClient.jsm
+++ b/services/fxaccounts/FxAccountsOAuthGrantClient.jsm
@@ -131,75 +131,66 @@ this.FxAccountsOAuthGrantClient.prototyp
    *        Profile server path, i.e "/profile".
    * @param {String} [method]
    *        Type of request, i.e "GET".
    * @return Promise
    *         Resolves: {Object} Successful response from the Profile server.
    *         Rejects: {FxAccountsOAuthGrantClientError} Profile client error.
    * @private
    */
-  _createRequest(path, method = "POST", params) {
-    return new Promise((resolve, reject) => {
-      let profileDataUrl = this.serverURL + path;
-      let request = new this._Request(profileDataUrl);
-      method = method.toUpperCase();
+  async _createRequest(path, method = "POST", params) {
+    let profileDataUrl = this.serverURL + path;
+    let request = new this._Request(profileDataUrl);
+    method = method.toUpperCase();
 
-      request.setHeader("Accept", "application/json");
-      request.setHeader("Content-Type", "application/json");
+    request.setHeader("Accept", "application/json");
+    request.setHeader("Content-Type", "application/json");
 
-      request.onComplete = function(error) {
-        if (error) {
-          reject(new FxAccountsOAuthGrantClientError({
-            error: ERROR_NETWORK,
-            errno: ERRNO_NETWORK,
-            message: error.toString(),
-          }));
-          return;
-        }
+    if (method != "POST") {
+      throw new FxAccountsOAuthGrantClientError({
+        error: ERROR_NETWORK,
+        errno: ERRNO_NETWORK,
+        code: ERROR_CODE_METHOD_NOT_ALLOWED,
+        message: ERROR_MSG_METHOD_NOT_ALLOWED,
+      });
+    }
 
-        let body = null;
-        try {
-          body = JSON.parse(request.response.body);
-        } catch (e) {
-          reject(new FxAccountsOAuthGrantClientError({
-            error: ERROR_PARSE,
-            errno: ERRNO_PARSE,
-            code: request.response.status,
-            message: request.response.body,
-          }));
-          return;
-        }
-
-        // "response.success" means status code is 200
-        if (request.response.success) {
-          resolve(body);
-          return;
-        }
+    try {
+      await request.post(params);
+    } catch (error) {
+      throw new FxAccountsOAuthGrantClientError({
+        error: ERROR_NETWORK,
+        errno: ERRNO_NETWORK,
+        message: error.toString(),
+      });
+    }
 
-        if (typeof body.errno === "number") {
-          // Offset oauth server errnos to avoid conflict with other FxA server errnos
-          body.errno += OAUTH_SERVER_ERRNO_OFFSET;
-        } else if (body.errno) {
-          body.errno = ERRNO_UNKNOWN_ERROR;
-        }
-        reject(new FxAccountsOAuthGrantClientError(body));
-      };
+    let body = null;
+    try {
+      body = JSON.parse(request.response.body);
+    } catch (e) {
+      throw new FxAccountsOAuthGrantClientError({
+        error: ERROR_PARSE,
+        errno: ERRNO_PARSE,
+        code: request.response.status,
+        message: request.response.body,
+      });
+    }
 
-      if (method === "POST") {
-        request.post(params);
-      } else {
-        // method not supported
-        reject(new FxAccountsOAuthGrantClientError({
-          error: ERROR_NETWORK,
-          errno: ERRNO_NETWORK,
-          code: ERROR_CODE_METHOD_NOT_ALLOWED,
-          message: ERROR_MSG_METHOD_NOT_ALLOWED,
-        }));
-      }
-    });
+    if (request.response.success) {
+      return body;
+    }
+
+    if (typeof body.errno === "number") {
+      // Offset oauth server errnos to avoid conflict with other FxA server errnos
+      body.errno += OAUTH_SERVER_ERRNO_OFFSET;
+    } else if (body.errno) {
+      body.errno = ERRNO_UNKNOWN_ERROR;
+    }
+    throw new FxAccountsOAuthGrantClientError(body);
   },
 
 };
 
 /**
  * Normalized profile client errors
  * @param {Object} [details]
  *        Error details object
--- a/services/fxaccounts/FxAccountsProfileClient.jsm
+++ b/services/fxaccounts/FxAccountsProfileClient.jsm
@@ -123,83 +123,75 @@ this.FxAccountsProfileClient.prototype =
    * @param {String} token
    * @param {String} etag
    * @return Promise
    *         Resolves: {body: Object, etag: Object} Successful response from the Profile server
                         or null if 304 is hit (same ETag).
    *         Rejects: {FxAccountsProfileClientError} Profile client error.
    * @private
    */
-  _rawRequest(path, method, token, etag) {
-    return new Promise((resolve, reject) => {
-      let profileDataUrl = this.serverURL + path;
-      let request = new this._Request(profileDataUrl);
-      method = method.toUpperCase();
+  async _rawRequest(path, method, token, etag) {
+    let profileDataUrl = this.serverURL + path;
+    let request = new this._Request(profileDataUrl);
+    method = method.toUpperCase();
 
-      request.setHeader("Authorization", "Bearer " + token);
-      request.setHeader("Accept", "application/json");
-      if (etag) {
-        request.setHeader("If-None-Match", etag);
-      }
+    request.setHeader("Authorization", "Bearer " + token);
+    request.setHeader("Accept", "application/json");
+    if (etag) {
+      request.setHeader("If-None-Match", etag);
+    }
 
-      request.onComplete = function(error) {
-        if (error) {
-          reject(new FxAccountsProfileClientError({
-            error: ERROR_NETWORK,
-            errno: ERRNO_NETWORK,
-            message: error.toString(),
-          }));
-          return;
-        }
+    if (method != "GET") {
+      // method not supported
+      throw new FxAccountsProfileClientError({
+        error: ERROR_NETWORK,
+        errno: ERRNO_NETWORK,
+        code: ERROR_CODE_METHOD_NOT_ALLOWED,
+        message: ERROR_MSG_METHOD_NOT_ALLOWED,
+      });
+    }
 
-        let body = null;
-        try {
-          if (request.response.status == 304) {
-            resolve(null);
-            return;
-          }
-          body = JSON.parse(request.response.body);
-        } catch (e) {
-          reject(new FxAccountsProfileClientError({
-            error: ERROR_PARSE,
-            errno: ERRNO_PARSE,
-            code: request.response.status,
-            message: request.response.body,
-          }));
-          return;
-        }
+    try {
+      await request.get();
+    } catch (error) {
+      throw new FxAccountsProfileClientError({
+        error: ERROR_NETWORK,
+        errno: ERRNO_NETWORK,
+        message: error.toString(),
+      });
+    }
 
-        // "response.success" means status code is 200
-        if (request.response.success) {
-          resolve({
-            body,
-            etag: request.response.headers.etag
-          });
-          return;
-        }
-        reject(new FxAccountsProfileClientError({
-          error: body.error || ERROR_UNKNOWN,
-          errno: body.errno || ERRNO_UNKNOWN_ERROR,
-          code: request.response.status,
-          message: body.message || body,
-        }));
-      };
+    let body = null;
+    try {
+      if (request.response.status == 304) {
+        return null;
+      }
+      body = JSON.parse(request.response.body);
+    } catch (e) {
+      throw new FxAccountsProfileClientError({
+        error: ERROR_PARSE,
+        errno: ERRNO_PARSE,
+        code: request.response.status,
+        message: request.response.body,
+      });
+    }
 
-      if (method === "GET") {
-        request.get();
-      } else {
-        // method not supported
-        reject(new FxAccountsProfileClientError({
-          error: ERROR_NETWORK,
-          errno: ERRNO_NETWORK,
-          code: ERROR_CODE_METHOD_NOT_ALLOWED,
-          message: ERROR_MSG_METHOD_NOT_ALLOWED,
-        }));
-      }
-    });
+    // "response.success" means status code is 200
+    if (!request.response.success) {
+      throw new FxAccountsProfileClientError({
+        error: body.error || ERROR_UNKNOWN,
+        errno: body.errno || ERRNO_UNKNOWN_ERROR,
+        code: request.response.status,
+        message: body.message || body,
+      });
+    }
+    return {
+      body,
+      etag: request.response.headers.etag
+    };
   },
 
   /**
    * Retrieve user's profile from the server
    *
    * @param {String} [etag]
    *        Optional ETag used for caching purposes. (may generate a 304 exception)
    * @return Promise
--- a/services/fxaccounts/tests/xpcshell/test_oauth_grant_client.js
+++ b/services/fxaccounts/tests/xpcshell/test_oauth_grant_client.js
@@ -18,36 +18,36 @@ const STATUS_SUCCESS = 200;
  * @param {String} response
  *        Mocked raw response from the server
  * @returns {Function}
  */
 var mockResponse = function(response) {
   return function() {
     return {
       setHeader() {},
-      post() {
+      async post() {
         this.response = response;
-        this.onComplete();
+        return response;
       }
     };
   };
 };
 
 /**
  * Mock request error responder
  * @param {Error} error
  *        Error object
  * @returns {Function}
  */
 var mockResponseError = function(error) {
   return function() {
     return {
       setHeader() {},
-      post() {
-        this.onComplete(error);
+      async post() {
+        throw error;
       }
     };
   };
 };
 
 add_test(function missingParams() {
   let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS);
   try {
@@ -110,17 +110,17 @@ add_test(function parseErrorResponse() {
       Assert.equal(e.code, STATUS_SUCCESS);
       Assert.equal(e.errno, ERRNO_PARSE);
       Assert.equal(e.error, ERROR_PARSE);
       Assert.equal(e.message, "unexpected");
       run_next_test();
     });
 });
 
-add_test(function serverErrorResponse() {
+add_task(async function serverErrorResponse() {
   let client = new FxAccountsOAuthGrantClient(CLIENT_OPTIONS);
   let response = {
     status: 400,
     body: "{ \"code\": 400, \"errno\": 104, \"error\": \"Bad Request\", \"message\": \"Unauthorized\", \"reason\": \"Invalid fxa assertion\" }",
   };
 
   client._Request = new mockResponse(response);
   client.getTokenFromAssertion("blah", "scope")
@@ -129,17 +129,17 @@ add_test(function serverErrorResponse() 
       Assert.equal(e.code, 400);
       Assert.equal(e.errno, ERRNO_INVALID_FXA_ASSERTION);
       Assert.equal(e.error, "Bad Request");
       Assert.equal(e.message, "Unauthorized");
       run_next_test();
     });
 });
 
-add_test(function networkErrorResponse() {
+add_task(async function networkErrorResponse() {
   let client = new FxAccountsOAuthGrantClient({
     serverURL: "https://domain.dummy",
     client_id: "abc123"
   });
   Services.prefs.setBoolPref("identity.fxaccounts.skipDeviceRegistration", true);
   client.getTokenFromAssertion("assertion", "scope")
     .catch(function(e) {
       Assert.equal(e.name, "FxAccountsOAuthGrantClientError");
--- a/services/fxaccounts/tests/xpcshell/test_profile_client.js
+++ b/services/fxaccounts/tests/xpcshell/test_profile_client.js
@@ -20,19 +20,19 @@ let mockResponse = function(response) {
     Request._requestUri = requestUri;
     Request.ifNoneMatchSet = false;
     return {
       setHeader(header, value) {
         if (header == "If-None-Match" && value == "bogusETag") {
           Request.ifNoneMatchSet = true;
         }
       },
-      get() {
+      async get() {
         this.response = response;
-        this.onComplete();
+        return this.response;
       }
     };
   };
 
   return Request;
 };
 
 // A simple mock FxA that hands out tokens without checking them and doesn't
@@ -55,18 +55,18 @@ const PROFILE_OPTIONS = {
  * @param {Error} error
  *        Error object
  * @returns {Function}
  */
 let mockResponseError = function(error) {
   return function() {
     return {
       setHeader() {},
-      get() {
-        this.onComplete(error);
+      async get() {
+        throw error;
       }
     };
   };
 };
 
 add_test(function successfulResponse() {
   let client = new FxAccountsProfileClient(PROFILE_OPTIONS);
   let response = {
@@ -217,20 +217,20 @@ add_test(function server401ResponseThenS
   client._Request = function(requestUri) {
     return {
       setHeader(name, value) {
         if (name == "Authorization") {
           numAuthHeaders++;
           Assert.equal(value, "Bearer " + lastToken);
         }
       },
-      get() {
+      async get() {
         this.response = responses[numRequests];
         ++numRequests;
-        this.onComplete();
+        return this.response;
       }
     };
   };
 
   client.fetchProfile()
     .then(result => {
       Assert.equal(result.body.avatar, "http://example.com/image.jpg");
       Assert.equal(result.body.id, "0d5c1a89b8c54580b8e3e8adadae864a");
@@ -281,20 +281,20 @@ add_test(function server401ResponsePersi
   client._Request = function(requestUri) {
     return {
       setHeader(name, value) {
         if (name == "Authorization") {
           numAuthHeaders++;
           Assert.equal(value, "Bearer " + lastToken);
         }
       },
-      get() {
+      async get() {
         this.response = response;
         ++numRequests;
-        this.onComplete();
+        return this.response;
       }
     };
   };
 
   client.fetchProfile().catch(function(e) {
       Assert.equal(e.name, "FxAccountsProfileClientError");
       Assert.equal(e.code, 401);
       Assert.equal(e.errno, 100);
--- a/services/sync/modules-testing/fxa_utils.js
+++ b/services/sync/modules-testing/fxa_utils.js
@@ -22,19 +22,19 @@ var initializeIdentityWithTokenServerRes
     requestLog.level = Log.Level.Trace;
   }
 
   // A mock request object.
   function MockRESTRequest(url) {}
   MockRESTRequest.prototype = {
     _log: requestLog,
     setHeader() {},
-    get(callback) {
+    async get() {
       this.response = response;
-      callback.call(this);
+      return response;
     }
   };
   // The mocked TokenServer client which will get the response.
   function MockTSC() { }
   MockTSC.prototype = new TokenServerClient();
   MockTSC.prototype.constructor = MockTSC;
   MockTSC.prototype.newRESTRequest = function(url) {
     return new MockRESTRequest(url);
--- a/services/sync/tests/unit/test_browserid_identity.js
+++ b/services/sync/tests/unit/test_browserid_identity.js
@@ -675,33 +675,33 @@ async function initializeIdentityWithHAW
   // A mock request object.
   function MockRESTRequest(uri, credentials, extra) {
     this._uri = uri;
     this._credentials = credentials;
     this._extra = extra;
   }
   MockRESTRequest.prototype = {
     setHeader() {},
-    post(data, callback) {
+    async post(data) {
       this.response = cbGetResponse("post", data, this._uri, this._credentials, this._extra);
-      callback.call(this);
+      return this.response;
     },
-    get(callback) {
+    async get() {
       // Skip /status requests (browserid_identity checks if the account still
       // exists after an auth error)
       if (this._uri.startsWith("http://mockedserver:9999/account/status")) {
         this.response = {
           status: 200,
           headers: {"content-type": "application/json"},
           body: JSON.stringify({exists: true}),
         };
       } else {
         this.response = cbGetResponse("get", null, this._uri, this._credentials, this._extra);
       }
-      callback.call(this);
+      return this.response;
     }
   };
 
   // The hawk client.
   function MockedHawkClient() {}
   MockedHawkClient.prototype = new HawkClient("http://mockedserver:9999");
   MockedHawkClient.prototype.constructor = MockedHawkClient;
   MockedHawkClient.prototype.newHAWKAuthenticatedRESTRequest = function(uri, credentials, extra) {
@@ -748,19 +748,19 @@ function mockTokenServer(func) {
   if (!requestLog.appenders.length) { // might as well see what it says :)
     requestLog.addAppender(new Log.DumpAppender());
     requestLog.level = Log.Level.Trace;
   }
   function MockRESTRequest(url) {}
   MockRESTRequest.prototype = {
     _log: requestLog,
     setHeader() {},
-    get(callback) {
+    async get() {
       this.response = func();
-      callback.call(this);
+      return this.response;
     }
   };
   // The mocked TokenServer client which will get the response.
   function MockTSC() { }
   MockTSC.prototype = new TokenServerClient();
   MockTSC.prototype.constructor = MockTSC;
   MockTSC.prototype.newRESTRequest = function(url) {
     return new MockRESTRequest(url);
--- a/services/sync/tests/unit/test_fxa_node_reassignment.js
+++ b/services/sync/tests/unit/test_fxa_node_reassignment.js
@@ -144,20 +144,19 @@ async function syncAndExpectNodeReassign
     await Service.sync();
   }
 
   // Make sure that we really do get a 401 (but we can only do that if we are
   // already logged in, as the login process is what sets up the URLs)
   if (Service.isLoggedIn) {
     _("Making request to " + url + " which should 401");
     let request = new RESTRequest(url);
-    request.get(function() {
-      Assert.equal(request.response.status, 401);
-      CommonUtils.nextTick(onwards);
-    });
+    await request.get();
+    Assert.equal(request.response.status, 401);
+    CommonUtils.nextTick(onwards);
   } else {
     _("Skipping preliminary validation check for a 401 as we aren't logged in");
     CommonUtils.nextTick(onwards);
   }
   await deferred.promise;
 }
 
 // Check that when we sync we don't request a new token by default - our
--- a/services/sync/tests/unit/test_httpd_sync_server.js
+++ b/services/sync/tests/unit/test_httpd_sync_server.js
@@ -63,87 +63,74 @@ add_test(function test_url_parsing() {
 ChromeUtils.import("resource://services-common/rest.js");
 function localRequest(server, path) {
   _("localRequest: " + path);
   let url = server.baseURI.substr(0, server.baseURI.length - 1) + path;
   _("url: " + url);
   return new RESTRequest(url);
 }
 
-add_test(function test_basic_http() {
+add_task(async function test_basic_http() {
   let server = new SyncServer();
   server.registerUser("john", "password");
   Assert.ok(server.userExists("john"));
-  server.start(null, function() {
-    _("Started on " + server.port);
-    CommonUtils.nextTick(function() {
-      let req = localRequest(server, "/1.1/john/storage/crypto/keys");
-      _("req is " + req);
-      req.get(function(err) {
-        Assert.equal(null, err);
-        CommonUtils.nextTick(function() {
-          server.stop(run_next_test);
-        });
-      });
-    });
-  });
+  server.start();
+  _("Started on " + server.port);
+
+  let req = localRequest(server, "/1.1/john/storage/crypto/keys");
+  _("req is " + req);
+  // Shouldn't reject, beyond that we don't care.
+  await req.get();
+
+  await promiseStopServer(server);
 });
 
-add_test(function test_info_collections() {
+add_task(async function test_info_collections() {
   let server = new SyncServer({
     __proto__: SyncServerCallback
   });
   function responseHasCorrectHeaders(r) {
     Assert.equal(r.status, 200);
     Assert.equal(r.headers["content-type"], "application/json");
     Assert.ok("x-weave-timestamp" in r.headers);
   }
 
   server.registerUser("john", "password");
-  server.start(null, function() {
-    CommonUtils.nextTick(function() {
-      let req = localRequest(server, "/1.1/john/info/collections");
-      req.get(function(err) {
-        // Initial info/collections fetch is empty.
-        Assert.equal(null, err);
-        responseHasCorrectHeaders(this.response);
+  server.start();
 
-        Assert.equal(this.response.body, "{}");
-        CommonUtils.nextTick(function() {
-          // When we PUT something to crypto/keys, "crypto" appears in the response.
-          function cb(err2) {
-            Assert.equal(null, err2);
-            responseHasCorrectHeaders(this.response);
-            let putResponseBody = this.response.body;
-            _("PUT response body: " + JSON.stringify(putResponseBody));
+  let req = localRequest(server, "/1.1/john/info/collections");
+  await req.get();
+  responseHasCorrectHeaders(req.response);
+  Assert.equal(req.response.body, "{}");
+
+  let putReq = localRequest(server, "/1.1/john/storage/crypto/keys");
+  let payload = JSON.stringify({foo: "bar"});
+  let putResp = await putReq.put(payload);
+
+  responseHasCorrectHeaders(putResp);
 
-            req = localRequest(server, "/1.1/john/info/collections");
-            req.get(function(err3) {
-              Assert.equal(null, err3);
-              responseHasCorrectHeaders(this.response);
-              let expectedColl = server.getCollection("john", "crypto");
-              Assert.ok(!!expectedColl);
-              let modified = expectedColl.timestamp;
-              Assert.ok(modified > 0);
-              Assert.equal(putResponseBody, modified);
-              Assert.equal(JSON.parse(this.response.body).crypto, modified);
-              CommonUtils.nextTick(function() {
-                server.stop(run_next_test);
-              });
-            });
-          }
-          let payload = JSON.stringify({foo: "bar"});
-          localRequest(server, "/1.1/john/storage/crypto/keys").put(payload, cb);
-        });
-      });
-    });
-  });
+  let putResponseBody = putResp.body;
+  _("PUT response body: " + JSON.stringify(putResponseBody));
+
+  // When we PUT something to crypto/keys, "crypto" appears in the response.
+  req = localRequest(server, "/1.1/john/info/collections");
+
+  await req.get();
+  responseHasCorrectHeaders(req.response);
+  let expectedColl = server.getCollection("john", "crypto");
+  Assert.ok(!!expectedColl);
+  let modified = expectedColl.timestamp;
+  Assert.ok(modified > 0);
+  Assert.equal(putResponseBody, modified);
+  Assert.equal(JSON.parse(req.response.body).crypto, modified);
+
+  await promiseStopServer(server);
 });
 
-add_test(function test_storage_request() {
+add_task(async function test_storage_request() {
   let keysURL = "/1.1/john/storage/crypto/keys?foo=bar";
   let foosURL = "/1.1/john/storage/crypto/foos";
   let storageURL = "/1.1/john/storage";
 
   let server = new SyncServer();
   let creation = server.timestamp();
   server.registerUser("john", "password");
 
@@ -151,130 +138,119 @@ add_test(function test_storage_request()
     crypto: {foos: {foo: "bar"}}
   });
   let coll = server.user("john").collection("crypto");
   Assert.ok(!!coll);
 
   _("We're tracking timestamps.");
   Assert.ok(coll.timestamp >= creation);
 
-  function retrieveWBONotExists(next) {
+  async function retrieveWBONotExists() {
     let req = localRequest(server, keysURL);
-    req.get(function(err) {
-      _("Body is " + this.response.body);
-      _("Modified is " + this.response.newModified);
-      Assert.equal(null, err);
-      Assert.equal(this.response.status, 404);
-      Assert.equal(this.response.body, "Not found");
-      CommonUtils.nextTick(next);
-    });
+    let response = await req.get();
+    _("Body is " + response.body);
+    _("Modified is " + response.newModified);
+    Assert.equal(response.status, 404);
+    Assert.equal(response.body, "Not found");
   }
-  function retrieveWBOExists(next) {
+
+  async function retrieveWBOExists() {
     let req = localRequest(server, foosURL);
-    req.get(function(err) {
-      _("Body is " + this.response.body);
-      _("Modified is " + this.response.newModified);
-      let parsedBody = JSON.parse(this.response.body);
-      Assert.equal(parsedBody.id, "foos");
-      Assert.equal(parsedBody.modified, coll.wbo("foos").modified);
-      Assert.equal(JSON.parse(parsedBody.payload).foo, "bar");
-      CommonUtils.nextTick(next);
-    });
+    let response = await req.get();
+    _("Body is " + response.body);
+    _("Modified is " + response.newModified);
+    let parsedBody = JSON.parse(response.body);
+    Assert.equal(parsedBody.id, "foos");
+    Assert.equal(parsedBody.modified, coll.wbo("foos").modified);
+    Assert.equal(JSON.parse(parsedBody.payload).foo, "bar");
   }
-  function deleteWBONotExists(next) {
+
+  async function deleteWBONotExists() {
     let req = localRequest(server, keysURL);
     server.callback.onItemDeleted = function(username, collection, wboID) {
       do_throw("onItemDeleted should not have been called.");
     };
 
-    req.delete(function(err) {
-      _("Body is " + this.response.body);
-      _("Modified is " + this.response.newModified);
-      Assert.equal(this.response.status, 200);
-      delete server.callback.onItemDeleted;
-      CommonUtils.nextTick(next);
-    });
+    let response = await req.delete();
+
+    _("Body is " + response.body);
+    _("Modified is " + response.newModified);
+    Assert.equal(response.status, 200);
+    delete server.callback.onItemDeleted;
   }
-  function deleteWBOExists(next) {
+
+  async function deleteWBOExists() {
     let req = localRequest(server, foosURL);
     server.callback.onItemDeleted = function(username, collection, wboID) {
       _("onItemDeleted called for " + collection + "/" + wboID);
       delete server.callback.onItemDeleted;
       Assert.equal(username, "john");
       Assert.equal(collection, "crypto");
       Assert.equal(wboID, "foos");
-      CommonUtils.nextTick(next);
     };
+    await req.delete();
+    _("Body is " + req.response.body);
+    _("Modified is " + req.response.newModified);
+    Assert.equal(req.response.status, 200);
+  }
 
-    req.delete(function(err) {
-      _("Body is " + this.response.body);
-      _("Modified is " + this.response.newModified);
-      Assert.equal(this.response.status, 200);
-    });
-  }
-  function deleteStorage(next) {
+  async function deleteStorage() {
     _("Testing DELETE on /storage.");
     let now = server.timestamp();
     _("Timestamp: " + now);
     let req = localRequest(server, storageURL);
-    req.delete(function(err) {
-      _("Body is " + this.response.body);
-      _("Modified is " + this.response.newModified);
-      let parsedBody = JSON.parse(this.response.body);
-      Assert.ok(parsedBody >= now);
-      do_check_empty(server.users.john.collections);
-      CommonUtils.nextTick(next);
-    });
+    await req.delete();
+
+    _("Body is " + req.response.body);
+    _("Modified is " + req.response.newModified);
+    let parsedBody = JSON.parse(req.response.body);
+    Assert.ok(parsedBody >= now);
+    do_check_empty(server.users.john.collections);
   }
-  function getStorageFails(next) {
+
+  async function getStorageFails() {
     _("Testing that GET on /storage fails.");
     let req = localRequest(server, storageURL);
-    req.get(function(err) {
-      Assert.equal(this.response.status, 405);
-      Assert.equal(this.response.headers.allow, "DELETE");
-      CommonUtils.nextTick(next);
-    });
+    await req.get();
+    Assert.equal(req.response.status, 405);
+    Assert.equal(req.response.headers.allow, "DELETE");
   }
-  function getMissingCollectionWBO(next) {
+
+  async function getMissingCollectionWBO() {
     _("Testing that fetching a WBO from an on-existent collection 404s.");
     let req = localRequest(server, storageURL + "/foobar/baz");
-    req.get(function(err) {
-      Assert.equal(this.response.status, 404);
-      CommonUtils.nextTick(next);
-    });
+    await req.get();
+    Assert.equal(req.response.status, 404);
   }
 
-  server.start(null,
-    Async.chain(
-      retrieveWBONotExists,
-      retrieveWBOExists,
-      deleteWBOExists,
-      deleteWBONotExists,
-      getStorageFails,
-      getMissingCollectionWBO,
-      deleteStorage,
-      server.stop.bind(server),
-      run_next_test
-    ));
+  server.start(null);
+
+  await retrieveWBONotExists();
+  await retrieveWBOExists();
+  await deleteWBOExists();
+  await deleteWBONotExists();
+  await getStorageFails();
+  await getMissingCollectionWBO();
+  await deleteStorage();
+
+  await promiseStopServer(server);
 });
 
-add_test(function test_x_weave_records() {
+add_task(async function test_x_weave_records() {
   let server = new SyncServer();
   server.registerUser("john", "password");
 
   server.createContents("john", {
     crypto: {foos: {foo: "bar"},
              bars: {foo: "baz"}}
   });
-  server.start(null, function() {
-    let wbo = localRequest(server, "/1.1/john/storage/crypto/foos");
-    wbo.get(function(err) {
-      // WBO fetches don't have one.
-      Assert.equal(false, "x-weave-records" in this.response.headers);
-      let col = localRequest(server, "/1.1/john/storage/crypto");
-      col.get(function(err2) {
-        // Collection fetches do.
-        Assert.equal(this.response.headers["x-weave-records"], "2");
-        server.stop(run_next_test);
-      });
-    });
-  });
+  server.start();
+
+  let wbo = localRequest(server, "/1.1/john/storage/crypto/foos");
+  await wbo.get();
+  Assert.equal(false, "x-weave-records" in wbo.response.headers);
+  let col = localRequest(server, "/1.1/john/storage/crypto");
+  await col.get();
+  // Collection fetches do.
+  Assert.equal(col.response.headers["x-weave-records"], "2");
+
+  await promiseStopServer(server);
 });
--- a/services/sync/tests/unit/test_node_reassignment.js
+++ b/services/sync/tests/unit/test_node_reassignment.js
@@ -74,23 +74,19 @@ async function syncAndExpectNodeReassign
     async getTokenFromBrowserIDAssertion(uri, assertion) {
       getTokenCount++;
       return {endpoint: server.baseURI + "1.1/johndoe/"};
     },
   };
   Service.identity._tokenServerClient = mockTSC;
 
   // Make sure that it works!
-  await new Promise(res => {
-    let request = new RESTRequest(url);
-    request.get(function() {
-      Assert.equal(request.response.status, 401);
-      res();
-    });
-  });
+  let request = new RESTRequest(url);
+  let response = await request.get();
+  Assert.equal(response.status, 401);
 
   function onFirstSync() {
     _("First sync completed.");
     Svc.Obs.remove(firstNotification, onFirstSync);
     Svc.Obs.add(secondNotification, onSecondSync);
 
     Assert.equal(Service.clusterURL, "");