Repository: stripe/stripe-node
Pull request #661 (2019-07-25 03:25:13)

Refactor errors to ES6 classes
Your Node.JS requirements at `package.json` is 6.x as minimum, and it's fully supports ES6 classes. Your are using _legacy_ extending everywhere, so, I think errors classes as smallest and non-breaking change will be good foundation for migration.

Forced to adjust ESLint config, as rule was conflicting with JSDoc comments for constructor and Prettier. And while we was there, removed unused `flowtype` ESlint plugin and unneeded `babel-eslint` (ESLint since version 5.x supports all that syntax)

I would love to refactor `stripe.js` into real class, but it's gonna be a breaking change, so, didn't touch for now. 

rattrayalex-stripe: Wow, this is awesome; thank you @tinovyatkin !

At first glance looks great. 

Going to want to think about whether this could result in any unintended breaking changes for users, or other unintended consequences... (Can you think of anything?)

I'm also curious if we should hold off on this until we're able to change _everything_ to uses proper classes. 
tinovyatkin: As I wrote initially, I've selected errors classes as a relatively small non-breaking change and where class refactoring in new code will really stands out (from my point of view version of classes looks way more beautiful, and that what is Stripe all about, right? ;-)).
The only official documentation about Node.JS errors is this https://stripe.com/docs/api/errors and these changes breaks nothing from it. I can't imaging someone relying on deep internals of previous Errors implementation in a way that this will be a breaking change.

Not sure about Stripe policy, but I love small PRs, and refactoring everything to class will be a huge task. You have two candidates left for class refactoring - `Stripe` from `stripe.js` (breaking due to documentation) and `StripResource`. If this will be merged I can do StripeResource refactoring then.

Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index 6d8aa70f..15769965 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -62,7 +62,6 @@ module.exports = {
     ],
     'line-comment-position': 'off',
     'linebreak-style': ['error', 'unix'],
-    'lines-around-comment': 'error',
     'lines-around-directive': 'error',
     'max-depth': 'error',
     'max-len': 'off',
@@ -245,7 +244,9 @@ module.exports = {
     'yield-star-spacing': 'error',
     yoda: ['error', 'never'],
   },
-  parser: 'babel-eslint',
+  parserOptions: {
+    ecmaVersion: 2018,
+  },
   plugins: ['prettier'],
   extends: ['plugin:prettier/recommended'],
 };
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 96fa3a40..cb18d959 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,26 +1,54 @@
 # CHANGELOG
 
+## 7.5.5 - 2019-08-02
+
+- [#665](https://github.com/stripe/stripe-node/pull/665) Remove `lodash.isplainobject`.
+
+## 7.5.4 - 2019-08-01
+
+- [#671](https://github.com/stripe/stripe-node/pull/671) Include a prefix in generated idempotency keys and remove uuid dependency.
+
+## 7.5.3 - 2019-07-31
+
+- [#667](https://github.com/stripe/stripe-node/pull/667) Refactor request headers, allowing any header to be overridden.
+
+## 7.5.2 - 2019-07-30
+
+- [#664](https://github.com/stripe/stripe-node/pull/664) Expose and use `once`
+
+## 7.5.1 - 2019-07-30
+
+- [#662](https://github.com/stripe/stripe-node/pull/662) Remove `safe-buffer` dependency
+- [#666](https://github.com/stripe/stripe-node/pull/666) Bump lodash from 4.17.11 to 4.17.15
+- [#668](https://github.com/stripe/stripe-node/pull/668) Move Balance History to /v1/balance_transactions
+
 ## 7.5.0 - 2019-07-24
-* [#660](https://github.com/stripe/stripe-node/pull/660) Interpret any string in args as API Key instead of a regex
+
+- [#660](https://github.com/stripe/stripe-node/pull/660) Interpret any string in args as API Key instead of a regex
   - ⚠️ Careful: passing strings which are not API Keys as as the final argument to a request previously would have ignored those strings, and would now result in the request failing with an authentication error.
   - ⚠️ Careful: The private api `utils.isAuthKey` was removed.
-* [#658](https://github.com/stripe/stripe-node/pull/658) Update README retry code sample to use two retries
-* [#653](https://github.com/stripe/stripe-node/pull/653) Reorder customer methods
+- [#658](https://github.com/stripe/stripe-node/pull/658) Update README retry code sample to use two retries
+- [#653](https://github.com/stripe/stripe-node/pull/653) Reorder customer methods
 
 ## 7.4.0 - 2019-06-27
-* [#652](https://github.com/stripe/stripe-node/pull/652) Add support for the `SetupIntent` resource and APIs
+
+- [#652](https://github.com/stripe/stripe-node/pull/652) Add support for the `SetupIntent` resource and APIs
 
 ## 7.3.0 - 2019-06-24
-* [#649](https://github.com/stripe/stripe-node/pull/649) Enable request latency telemetry by default
+
+- [#649](https://github.com/stripe/stripe-node/pull/649) Enable request latency telemetry by default
 
 ## 7.2.0 - 2019-06-17
-* [#608](https://github.com/stripe/stripe-node/pull/608) Add support for `CustomerBalanceTransaction` resource and APIs
+
+- [#608](https://github.com/stripe/stripe-node/pull/608) Add support for `CustomerBalanceTransaction` resource and APIs
 
 ## 7.1.0 - 2019-05-23
-* [#632](https://github.com/stripe/stripe-node/pull/632) Add support for `radar.early_fraud_warning` resource
+
+- [#632](https://github.com/stripe/stripe-node/pull/632) Add support for `radar.early_fraud_warning` resource
 
 ## 7.0.1 - 2019-05-22
-* [#631](https://github.com/stripe/stripe-node/pull/631) Make autopagination functions work for `listLineItems` and `listUpcomingLineItems`
+
+- [#631](https://github.com/stripe/stripe-node/pull/631) Make autopagination functions work for `listLineItems` and `listUpcomingLineItems`
 
 ## 7.0.0 - 2019-05-14
 
diff --git a/README.md b/README.md
index c81a75e4..ad6cd67e 100644
--- a/README.md
+++ b/README.md
@@ -175,7 +175,7 @@ This will retry requests `n` times with exponential backoff if they fail due to
 [Idempotency keys](https://stripe.com/docs/api/idempotent_requests) are added where appropriate to prevent duplication.
 
 ```js
- // Retry a request twice before giving up
+// Retry a request twice before giving up
 stripe.setMaxNetworkRetries(2);
 ```
 
@@ -410,16 +410,16 @@ $ yarn test
 
 If you do not have `yarn` installed, you can get it with `npm install --global yarn`.
 
-Run a single test suite:
+Run a single test suite without a coverage report:
 
 ```bash
-$ yarn mocha test/Error.spec.js
+$ yarn mocha-only test/Error.spec.js
 ```
 
-Run a single test (case sensitive):
+Run a single test (case sensitive) in watch mode:
 
 ```bash
-$ yarn mocha test/Error.spec.js --grep 'Populates with type'
+$ yarn mocha-only test/Error.spec.js --grep 'Populates with type' --watch
 ```
 
 If you wish, you may run tests using your Stripe _Test_ API key by setting the
diff --git a/VERSION b/VERSION
index 18bb4182..254dd00b 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-7.5.0
+7.5.5
diff --git a/lib/Error.js b/lib/Error.js
index 6e5bd43e..86bfda86 100644
--- a/lib/Error.js
+++ b/lib/Error.js
@@ -1,94 +1,197 @@
 'use strict';
 
-const utils = require('./utils');
-
-module.exports = _Error;
-
 /**
- * Generic Error klass to wrap any errors returned by stripe-node
+ *  @typedef {{ message?: string, type?: string, code?: number, param?: string, detail?: string, headers?: Record<string, string>, requestId?: string, statusCode?: number }} ErrorParams
  */
-function _Error(raw) {
-  this.populate(...arguments);
-  this.stack = new Error(this.message).stack;
-}
+/**
+ * Generic Error class to wrap any errors returned by stripe-node
+ */
+class GenericError extends Error {
+  /**
+   *
+   * @param  {string} type
+   * @param {string} message
+   */
+  constructor(type, message) {
+    super(message);
+    this.type = type || this.constructor.name;
+    // Saving class name in the property of our custom error as a shortcut.
+    this.name = this.constructor.name;
 
-// Extend Native Error
-_Error.prototype = Object.create(Error.prototype);
+    // Capturing stack trace, excluding constructor call from it.
+    Error.captureStackTrace(this, this.constructor);
+  }
 
-_Error.prototype.type = 'GenericError';
-_Error.prototype.populate = function(type, message) {
-  this.type = type;
-  this.message = message;
-};
+  /**
+   *
+   * @param {string} [type] - error type name
+   * @param {string} [message]
+   */
+  populate(type, message) {
+    this.type = type;
+    this.message = message;
+  }
 
-_Error.extend = utils.protoExtend;
+  /**
+   * DEPRECATED
+   * Please use ES6 class inheritance instead.
+   * @param {{ type: string, message?: string, [k:string]: any }} options
+   */
+  static extend(options) {
+    class customError extends StripeError {
+      /**
+       * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/name#Function_names_in_classes}
+       */
+      // @ts-ignore
+      static get name() {
+        return options.type;
+      }
+    }
+    Object.assign(customError.prototype, options);
+    return customError;
+  }
+}
 
 /**
- * Create subclass of internal Error klass
+ * StripeError is the base error from which all other more specific Stripe
+ * errors derive.
  * (Specifically for errors returned from Stripe's REST API)
+ *
  */
-const StripeError = (_Error.StripeError = _Error.extend({
-  type: 'StripeError',
+class StripeError extends GenericError {
+  /**
+   *
+   * @param {ErrorParams} [raw]
+   */
+  constructor(raw = {}) {
+    super(undefined, raw.message);
+    this.populate(raw);
+  }
+  /**
+   *
+   * @param {ErrorParams} raw
+   */
+  // @ts-ignore
   populate(raw) {
-    // Move from prototype def (so it appears in stringified obj)
-    this.type = this.type;
-
-    this.stack = new Error(raw.message).stack;
+    if (!raw || typeof raw !== 'object' || Object.keys(raw).length < 1) {
+      return;
+    }
+    this.raw = raw;
     this.rawType = raw.type;
     this.code = raw.code;
     this.param = raw.param;
-    this.message = raw.message;
     this.detail = raw.detail;
-    this.raw = raw;
     this.headers = raw.headers;
     this.requestId = raw.requestId;
     this.statusCode = raw.statusCode;
-  },
-}));
+    this.message = raw.message;
+  }
 
-/**
- * Helper factory which takes raw stripe errors and outputs wrapping instances
- */
-StripeError.generate = (rawStripeError) => {
-  switch (rawStripeError.type) {
-    case 'card_error':
-      return new _Error.StripeCardError(rawStripeError);
-    case 'invalid_request_error':
-      return new _Error.StripeInvalidRequestError(rawStripeError);
-    case 'api_error':
-      return new _Error.StripeAPIError(rawStripeError);
-    case 'idempotency_error':
-      return new _Error.StripeIdempotencyError(rawStripeError);
-    case 'invalid_grant':
-      return new _Error.StripeInvalidGrantError(rawStripeError);
+  /**
+   * Helper factory which takes raw stripe errors and outputs wrapping instances
+   *
+   * @param {ErrorParams} rawStripeError
+   */
+  static generate(rawStripeError) {
+    switch (rawStripeError.type) {
+      case 'card_error':
+        return new StripeCardError(rawStripeError);
+      case 'invalid_request_error':
+        return new StripeInvalidRequestError(rawStripeError);
+      case 'api_error':
+        return new StripeAPIError(rawStripeError);
+      case 'idempotency_error':
+        return new StripeIdempotencyError(rawStripeError);
+      case 'invalid_grant':
+        return new StripeInvalidGrantError(rawStripeError);
+      default:
+        return new GenericError('Generic', 'Unknown Error');
+    }
   }
-  return new _Error('Generic', 'Unknown Error');
-};
+}
 
 // Specific Stripe Error types:
-_Error.StripeCardError = StripeError.extend({type: 'StripeCardError'});
-_Error.StripeInvalidRequestError = StripeError.extend({
-  type: 'StripeInvalidRequestError',
-});
-_Error.StripeAPIError = StripeError.extend({type: 'StripeAPIError'});
-_Error.StripeAuthenticationError = StripeError.extend({
-  type: 'StripeAuthenticationError',
-});
-_Error.StripePermissionError = StripeError.extend({
-  type: 'StripePermissionError',
-});
-_Error.StripeRateLimitError = StripeError.extend({
-  type: 'StripeRateLimitError',
-});
-_Error.StripeConnectionError = StripeError.extend({
-  type: 'StripeConnectionError',
-});
-_Error.StripeSignatureVerificationError = StripeError.extend({
-  type: 'StripeSignatureVerificationError',
-});
-_Error.StripeIdempotencyError = StripeError.extend({
-  type: 'StripeIdempotencyError',
-});
-_Error.StripeInvalidGrantError = StripeError.extend({
-  type: 'StripeInvalidGrantError',
-});
+
+/**
+ * CardError is raised when a user enters a card that can't be charged for
+ * some reason.
+ */
+class StripeCardError extends StripeError {}
+
+/**
+ * InvalidRequestError is raised when a request is initiated with invalid
+ * parameters.
+ */
+class StripeInvalidRequestError extends StripeError {}
+
+/**
+ * APIError is a generic error that may be raised in cases where none of the
+ * other named errors cover the problem. It could also be raised in the case
+ * that a new error has been introduced in the API, but this version of the
+ * Node.JS SDK doesn't know how to handle it.
+ */
+class StripeAPIError extends StripeError {}
+
+/**
+ * AuthenticationError is raised when invalid credentials are used to connect
+ * to Stripe's servers.
+ */
+class StripeAuthenticationError extends StripeError {}
+
+/**
+ * PermissionError is raised in cases where access was attempted on a resource
+ * that wasn't allowed.
+ */
+class StripePermissionError extends StripeError {}
+
+/**
+ * RateLimitError is raised in cases where an account is putting too much load
+ * on Stripe's API servers (usually by performing too many requests). Please
+ * back off on request rate.
+ */
+class StripeRateLimitError extends StripeError {}
+
+/**
+ * StripeConnectionError is raised in the event that the SDK can't connect to
+ * Stripe's servers. That can be for a variety of different reasons from a
+ * downed network to a bad TLS certificate.
+ */
+class StripeConnectionError extends StripeError {}
+
+/**
+ * SignatureVerificationError is raised when the signature verification for a
+ * webhook fails
+ */
+class StripeSignatureVerificationError extends StripeError {}
+
+/**
+ * IdempotencyError is raised in cases where an idempotency key was used
+ * improperly.
+ */
+class StripeIdempotencyError extends StripeError {}
+
+/**
+ * InvalidGrantError is raised when a specified code doesn't exist, is
+ * expired, has been used, or doesn't belong to you; a refresh token doesn't
+ * exist, or doesn't belong to you; or if an API key's mode (live or test)
+ * doesn't match the mode of a code or refresh token.
+ */
+class StripeInvalidGrantError extends StripeError {}
+
+/**
+ * DEPRECATED: Default import from this module is deprecated and
+ * will be removed in the next major version
+ */
+module.exports = GenericError;
+
+module.exports.StripeError = StripeError;
+module.exports.StripeCardError = StripeCardError;
+module.exports.StripeInvalidRequestError = StripeInvalidRequestError;
+module.exports.StripeAPIError = StripeAPIError;
+module.exports.StripeAuthenticationError = StripeAuthenticationError;
+module.exports.StripePermissionError = StripePermissionError;
+module.exports.StripeRateLimitError = StripeRateLimitError;
+module.exports.StripeConnectionError = StripeConnectionError;
+module.exports.StripeSignatureVerificationError = StripeSignatureVerificationError;
+module.exports.StripeIdempotencyError = StripeIdempotencyError;
+module.exports.StripeInvalidGrantError = StripeInvalidGrantError;
diff --git a/lib/MultipartDataGenerator.js b/lib/MultipartDataGenerator.js
index 76b8cb29..9c819bd7 100644
--- a/lib/MultipartDataGenerator.js
+++ b/lib/MultipartDataGenerator.js
@@ -1,6 +1,5 @@
 'use strict';
 
-const Buffer = require('safe-buffer').Buffer;
 const utils = require('./utils');
 
 // Method for formatting HTTP body for the multipart/form-data specification
diff --git a/lib/StripeResource.js b/lib/StripeResource.js
index 6bc17dd3..164a1826 100644
--- a/lib/StripeResource.js
+++ b/lib/StripeResource.js
@@ -3,7 +3,6 @@
 const http = require('http');
 const https = require('https');
 const path = require('path');
-const uuid = require('uuid/v4');
 
 const utils = require('./utils');
 const Error = require('./Error');
@@ -88,7 +87,7 @@ StripeResource.prototype = {
 
   _timeoutHandler(timeout, req, callback) {
     return () => {
-      const timeoutErr = new Error('ETIMEDOUT');
+      const timeoutErr = new TypeError('ETIMEDOUT');
       timeoutErr.code = 'ETIMEDOUT';
 
       req._isAborted = true;
@@ -113,7 +112,7 @@ StripeResource.prototype = {
       res.on('data', (chunk) => {
         response += chunk;
       });
-      res.on('end', () => {
+      res.once('end', () => {
         const headers = res.headers || {};
         // NOTE: Stripe responds with lowercase header names/keys.
 
@@ -123,7 +122,7 @@ StripeResource.prototype = {
 
         const requestDurationMs = Date.now() - req._requestStart;
 
-        const responseEvent = utils.removeEmpty({
+        const responseEvent = utils.removeNullish({
           api_version: headers['stripe-version'],
           account: headers['stripe-account'],
           idempotency_key: headers['idempotency-key'],
@@ -262,38 +261,58 @@ StripeResource.prototype = {
     return sleepSeconds * 1000;
   },
 
-  _defaultHeaders(auth, contentLength, apiVersion) {
-    let userAgentString = `Stripe/v1 NodeBindings/${this._stripe.getConstant(
-      'PACKAGE_VERSION'
-    )}`;
-
-    if (this._stripe._appInfo) {
-      userAgentString += ` ${this._stripe.getAppInfoAsString()}`;
+  _defaultIdempotencyKey(method) {
+    // If this is a POST and we allow multiple retries, ensure an idempotency key.
+    if (method === 'POST' && this._stripe.getMaxNetworkRetries() > 0) {
+      return `stripe-node-retry-${utils.uuid4()}`;
     }
+    return null;
+  },
 
-    const headers = {
+  _makeHeaders(
+    auth,
+    contentLength,
+    apiVersion,
+    clientUserAgent,
+    method,
+    userSuppliedHeaders
+  ) {
+    const defaultHeaders = {
       // Use specified auth token or use default from this stripe instance:
       Authorization: auth ? `Bearer ${auth}` : this._stripe.getApiField('auth'),
       Accept: 'application/json',
       'Content-Type': 'application/x-www-form-urlencoded',
       'Content-Length': contentLength,
-      'User-Agent': userAgentString,
+      'User-Agent': this._getUserAgentString(),
+      'X-Stripe-Client-User-Agent': clientUserAgent,
+      'X-Stripe-Client-Telemetry': this._getTelemetryHeader(),
+      'Stripe-Version': apiVersion,
+      'Idempotency-Key': this._defaultIdempotencyKey(method),
     };
 
-    if (apiVersion) {
-      headers['Stripe-Version'] = apiVersion;
-    }
+    return Object.assign(
+      utils.removeNullish(defaultHeaders),
+      // If the user supplied, say 'idempotency-key', override instead of appending by ensuring caps are the same.
+      utils.normalizeHeaders(userSuppliedHeaders)
+    );
+  },
 
-    return headers;
+  _getUserAgentString() {
+    const packageVersion = this._stripe.getConstant('PACKAGE_VERSION');
+    const appInfo = this._stripe._appInfo
+      ? this._stripe.getAppInfoAsString()
+      : '';
+
+    return `Stripe/v1 NodeBindings/${packageVersion} ${appInfo}`.trim();
   },
 
-  _addTelemetryHeader(headers) {
+  _getTelemetryHeader() {
     if (
       this._stripe.getTelemetryEnabled() &&
       this._stripe._prevRequestMetrics.length > 0
     ) {
       const metrics = this._stripe._prevRequestMetrics.shift();
-      headers['X-Stripe-Client-Telemetry'] = JSON.stringify({
+      return JSON.stringify({
         last_request_metrics: metrics,
       });
     }
@@ -320,6 +339,16 @@ StripeResource.prototype = {
   _request(method, host, path, data, auth, options, callback) {
     let requestData;
 
+    const retryRequest = (requestFn, apiVersion, headers, requestRetries) => {
+      return setTimeout(
+        requestFn,
+        this._getSleepTimeInMS(requestRetries),
+        apiVersion,
+        headers,
+        requestRetries + 1
+      );
+    };
+
     const makeRequest = (apiVersion, headers, numRetries) => {
       const timeout = this._stripe.getApiField('timeout');
       const isInsecureConnection =
@@ -339,15 +368,7 @@ StripeResource.prototype = {
         ciphers: 'DEFAULT:!aNULL:!eNULL:!LOW:!EXPORT:!SSLv2:!MD5',
       });
 
-      // If this is a POST and we allow multiple retries, set a idempotency key if one is not
-      // already provided.
-      if (method === 'POST' && this._stripe.getMaxNetworkRetries() > 0) {
-        if (!headers.hasOwnProperty('Idempotency-Key')) {
-          headers['Idempotency-Key'] = uuid();
-        }
-      }
-
-      const requestEvent = utils.removeEmpty({
+      const requestEvent = utils.removeNullish({
         api_version: apiVersion,
         account: headers['Stripe-Account'],
         idempotency_key: headers['Idempotency-Key'],
@@ -365,7 +386,7 @@ StripeResource.prototype = {
 
       req.setTimeout(timeout, this._timeoutHandler(timeout, req, callback));
 
-      req.on('response', (res) => {
+      req.once('response', (res) => {
         if (this._shouldRetry(res, requestRetries)) {
           return retryRequest(makeRequest, apiVersion, headers, requestRetries);
         } else {
@@ -381,13 +402,16 @@ StripeResource.prototype = {
         }
       });
 
-      req.on('socket', (socket) => {
+      req.once('socket', (socket) => {
         if (socket.connecting) {
-          socket.on(isInsecureConnection ? 'connect' : 'secureConnect', () => {
-            // Send payload; we're safe:
-            req.write(requestData);
-            req.end();
-          });
+          socket.once(
+            isInsecureConnection ? 'connect' : 'secureConnect',
+            () => {
+              // Send payload; we're safe:
+              req.write(requestData);
+              req.end();
+            }
+          );
         } else {
           // we're already connected
           req.write(requestData);
@@ -396,27 +420,23 @@ StripeResource.prototype = {
       });
     };
 
-    const makeRequestWithData = (error, data) => {
+    const prepareAndMakeRequest = (error, data) => {
       if (error) {
         return callback(error);
       }
 
-      const apiVersion = this._stripe.getApiField('version');
       requestData = data;
-      const headers = this._defaultHeaders(
-        auth,
-        requestData.length,
-        apiVersion
-      );
 
-      this._stripe.getClientUserAgent((cua) => {
-        headers['X-Stripe-Client-User-Agent'] = cua;
-
-        if (options.headers) {
-          Object.assign(headers, options.headers);
-        }
-
-        this._addTelemetryHeader(headers);
+      this._stripe.getClientUserAgent((clientUserAgent) => {
+        const apiVersion = this._stripe.getApiField('version');
+        const headers = this._makeHeaders(
+          auth,
+          requestData.length,
+          apiVersion,
+          clientUserAgent,
+          method,
+          options.headers
+        );
 
         makeRequest(apiVersion, headers);
       });
@@ -427,23 +447,11 @@ StripeResource.prototype = {
         method,
         data,
         options.headers,
-        makeRequestWithData
+        prepareAndMakeRequest
       );
     } else {
-      makeRequestWithData(null, utils.stringifyRequestData(data || {}));
+      prepareAndMakeRequest(null, utils.stringifyRequestData(data || {}));
     }
-
-    const retryRequest = (requestFn, apiVersion, headers, requestRetries) => {
-      requestRetries += 1;
-
-      return setTimeout(
-        requestFn,
-        this._getSleepTimeInMS(requestRetries),
-        apiVersion,
-        headers,
-        requestRetries
-      );
-    };
   },
 };
 
diff --git a/lib/Webhooks.js b/lib/Webhooks.js
index cb5a45a4..892e0ae2 100644
--- a/lib/Webhooks.js
+++ b/lib/Webhooks.js
@@ -1,6 +1,5 @@
 'use strict';
 
-const Buffer = require('safe-buffer').Buffer;
 const crypto = require('crypto');
 
 const utils = require('./utils');
diff --git a/lib/resources/BalanceTransactions.js b/lib/resources/BalanceTransactions.js
index 3e3a6c7d..cad69117 100644
--- a/lib/resources/BalanceTransactions.js
+++ b/lib/resources/BalanceTransactions.js
@@ -3,7 +3,7 @@
 const StripeResource = require('../StripeResource');
 
 module.exports = StripeResource.extend({
-  path: 'balance/history',
+  path: 'balance_transactions',
 
   includeBasic: ['list', 'retrieve'],
 });
diff --git a/lib/resources/Files.js b/lib/resources/Files.js
index 424475c0..990de877 100644
--- a/lib/resources/Files.js
+++ b/lib/resources/Files.js
@@ -1,12 +1,13 @@
 'use strict';
 
-const Buffer = require('safe-buffer').Buffer;
 const utils = require('../utils');
 const multipartDataGenerator = require('../MultipartDataGenerator');
-const Error = require('../Error');
+const {StripeError} = require('../Error');
 const StripeResource = require('../StripeResource');
 const stripeMethod = StripeResource.method;
 
+class StreamProcessingError extends StripeError {}
+
 module.exports = StripeResource.extend({
   path: 'files',
 
@@ -37,7 +38,7 @@ module.exports = StripeResource.extend({
         .on('data', (line) => {
           bufferArray.push(line);
         })
-        .on('end', () => {
+        .once('end', () => {
           const bufferData = Object.assign({}, data);
           bufferData.file.data = Buffer.concat(bufferArray);
           const buffer = fn(method, bufferData, headers);
@@ -50,14 +51,6 @@ module.exports = StripeResource.extend({
     }
 
     function streamError(callback) {
-      const StreamProcessingError = Error.extend({
-        type: 'StreamProcessingError',
-        populate(raw) {
-          this.type = this.type;
-          this.message = raw.message;
-          this.detail = raw.detail;
-        },
-      });
       return (error) => {
         callback(
           new StreamProcessingError({
diff --git a/lib/stripe.js b/lib/stripe.js
index 9a467d3a..5e9e7e5c 100644
--- a/lib/stripe.js
+++ b/lib/stripe.js
@@ -43,10 +43,11 @@ function Stripe(key, version) {
     value: new EventEmitter(),
     enumerable: false,
     configurable: false,
-    writeable: false,
+    writable: false,
   });
 
   this.on = this._emitter.on.bind(this._emitter);
+  this.once = this._emitter.once.bind(this._emitter);
   this.off = this._emitter.removeListener.bind(this._emitter);
 
   this._api = {
diff --git a/lib/utils.js b/lib/utils.js
index 4bbccd90..4675ba37 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -1,13 +1,11 @@
 'use strict';
 
-const Buffer = require('safe-buffer').Buffer;
 const EventEmitter = require('events').EventEmitter;
 const exec = require('child_process').exec;
 const qs = require('qs');
 const crypto = require('crypto');
 
-const hasOwn = {}.hasOwnProperty;
-const isPlainObject = require('lodash.isplainobject');
+const hasOwn = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
 
 const OPTIONS_KEYS = [
   'api_key',
@@ -17,8 +15,10 @@ const OPTIONS_KEYS = [
 ];
 
 const utils = (module.exports = {
-  isOptionsHash: (o) => {
-    return isPlainObject(o) && OPTIONS_KEYS.some((key) => hasOwn.call(o, key));
+  isOptionsHash(o) {
+    return (
+      o && typeof o === 'object' && OPTIONS_KEYS.some((prop) => hasOwn(o, prop))
+    );
   },
 
   /**
@@ -73,9 +73,12 @@ const utils = (module.exports = {
 
   /**
    * Return the data argument from a list of arguments
+   *
+   * @param {object[]} args
+   * @returns {object}
    */
-  getDataFromArgs: (args) => {
-    if (args.length < 1 || !isPlainObject(args[0])) {
+  getDataFromArgs(args) {
+    if (!Array.isArray(args) || !args[0] || typeof args[0] !== 'object') {
       return {};
     }
 
@@ -154,7 +157,7 @@ const utils = (module.exports = {
    */
   protoExtend(sub) {
     const Super = this;
-    const Constructor = hasOwn.call(sub, 'constructor')
+    const Constructor = hasOwn(sub, 'constructor')
       ? sub.constructor
       : function(...args) {
           Super.apply(this, args);
@@ -203,18 +206,47 @@ const utils = (module.exports = {
   /**
    * Remove empty values from an object
    */
-  removeEmpty: (obj) => {
+  removeNullish: (obj) => {
     if (typeof obj !== 'object') {
       throw new Error('Argument must be an object');
     }
 
-    Object.keys(obj).forEach((key) => {
-      if (obj[key] === null || obj[key] === undefined) {
-        delete obj[key];
+    return Object.keys(obj).reduce((result, key) => {
+      if (obj[key] != null) {
+        result[key] = obj[key];
       }
-    });
+      return result;
+    }, {});
+  },
+
+  /**
+   * Normalize standard HTTP Headers:
+   * {'foo-bar': 'hi'}
+   * becomes
+   * {'Foo-Bar': 'hi'}
+   */
+  normalizeHeaders: (obj) => {
+    if (!(obj && typeof obj === 'object')) {
+      return obj;
+    }
 
-    return obj;
+    return Object.keys(obj).reduce((result, header) => {
+      result[utils.normalizeHeader(header)] = obj[header];
+      return result;
+    }, {});
+  },
+
+  /**
+   * Stolen from https://github.com/marten-de-vries/header-case-normalizer/blob/master/index.js#L36-L41
+   * without the exceptions which are irrelevant to us.
+   */
+  normalizeHeader: (header) => {
+    return header
+      .split('-')
+      .map(
+        (text) => text.charAt(0).toUpperCase() + text.substr(1).toLowerCase()
+      )
+      .join('-');
   },
 
   /**
@@ -313,6 +345,17 @@ const utils = (module.exports = {
 
     return result;
   },
+
+  /**
+   * https://stackoverflow.com/a/2117523
+   */
+  uuid4: () => {
+    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
+      const r = (Math.random() * 16) | 0;
+      const v = c === 'x' ? r : (r & 0x3) | 0x8;
+      return v.toString(16);
+    });
+  },
 });
 
 function emitWarning(warning) {
diff --git a/package.json b/package.json
index 8db2e321..56c8cad3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "stripe",
-  "version": "7.5.0",
+  "version": "7.5.5",
   "description": "Stripe API wrapper",
   "keywords": [
     "stripe",
@@ -26,14 +26,12 @@
   },
   "main": "lib/stripe.js",
   "devDependencies": {
-    "babel-eslint": "^10.0.1",
     "chai": "~4.2.0",
     "chai-as-promised": "~7.1.1",
     "coveralls": "^3.0.0",
     "eslint": "^5.16.0",
     "eslint-config-prettier": "^4.1.0",
     "eslint-plugin-chai-friendly": "^0.4.0",
-    "eslint-plugin-flowtype": "^3.8.1",
     "eslint-plugin-prettier": "^3.0.1",
     "mocha": "~6.1.4",
     "nock": "^10.0.6",
@@ -41,10 +39,7 @@
     "prettier": "^1.16.4"
   },
   "dependencies": {
-    "lodash.isplainobject": "^4.0.6",
-    "qs": "^6.6.0",
-    "safe-buffer": "^5.1.1",
-    "uuid": "^3.3.2"
+    "qs": "^6.6.0"
   },
   "license": "MIT",
   "scripts": {
diff --git a/yarn.lock b/yarn.lock
index 7f942287..11e0e935 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -52,7 +52,7 @@
     esutils "^2.0.2"
     js-tokens "^4.0.0"
 
-"@babel/parser@^7.0.0", "@babel/parser@^7.4.3", "@babel/parser@^7.4.4":
+"@babel/parser@^7.4.3", "@babel/parser@^7.4.4":
   version "7.4.4"
   resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.4.4.tgz#5977129431b8fe33471730d255ce8654ae1250b6"
   integrity sha512-5pCS4mOsL+ANsFZGdvNLybx4wtqAZJ0MJjMHxvzI3bvIsz6sQvzW8XX92EYIkiPtIvcfG3Aj+Ir5VNyjnZhP7w==
@@ -66,7 +66,7 @@
     "@babel/parser" "^7.4.4"
     "@babel/types" "^7.4.4"
 
-"@babel/traverse@^7.0.0", "@babel/traverse@^7.4.3":
+"@babel/traverse@^7.4.3":
   version "7.4.4"
   resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.4.4.tgz#0776f038f6d78361860b6823887d4f3937133fe8"
   integrity sha512-Gw6qqkw/e6AGzlyj9KnkabJX7VcubqPtkUQVAwkc0wUMldr3A/hezNB3Rc5eIvId95iSGkGIOe5hh1kMKf951A==
@@ -198,18 +198,6 @@ aws4@^1.8.0:
   resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
   integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
 
-babel-eslint@^10.0.1:
-  version "10.0.1"
-  resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.0.1.tgz#919681dc099614cd7d31d45c8908695092a1faed"
-  integrity sha512-z7OT1iNV+TjOwHNLLyJk+HN+YVWX+CLE6fPD2SymJZOZQBs+QIexFjhm4keGTm8MW9xr4EC9Q0PbaLB24V5GoQ==
-  dependencies:
-    "@babel/code-frame" "^7.0.0"
-    "@babel/parser" "^7.0.0"
-    "@babel/traverse" "^7.0.0"
-    "@babel/types" "^7.0.0"
-    eslint-scope "3.7.1"
-    eslint-visitor-keys "^1.0.0"
-
 balanced-match@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
@@ -556,13 +544,6 @@ eslint-plugin-chai-friendly@^0.4.0:
   resolved "https://registry.yarnpkg.com/eslint-plugin-chai-friendly/-/eslint-plugin-chai-friendly-0.4.1.tgz#9eeb17f92277ba80bb64f0e946c6936a3ae707b4"
   integrity sha512-hkpLN7VVoGGsofZjUhcQ+sufC3FgqMJwD0DvAcRfxY1tVRyQyVsqpaKnToPHJQOrRo0FQ0fSEDwW2gr4rsNdGA==
 
-eslint-plugin-flowtype@^3.8.1:
-  version "3.8.1"
-  resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-3.8.1.tgz#fefbc160b8f55c0bc0201236471637ad47ca6886"
-  integrity sha512-7ye/CCdTzytOu54Y1dUed/ejcIPMe0eSlxhfPIhOv2DCMvLMbwYGagEYfEuJ1ylMhkt8Z+rt8VtJXniNwC3hRQ==
-  dependencies:
-    lodash "^4.17.11"
-
 eslint-plugin-prettier@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.0.1.tgz#19d521e3981f69dd6d14f64aec8c6a6ac6eb0b0d"
@@ -570,14 +551,6 @@ eslint-plugin-prettier@^3.0.1:
   dependencies:
     prettier-linter-helpers "^1.0.0"
 
-eslint-scope@3.7.1:
-  version "3.7.1"
-  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-3.7.1.tgz#3d63c3edfda02e06e01a452ad88caacc7cdcb6e8"
-  integrity sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=
-  dependencies:
-    esrecurse "^4.1.0"
-    estraverse "^4.1.1"
-
 eslint-scope@^4.0.3:
   version "4.0.3"
   resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848"
@@ -1247,15 +1220,10 @@ lodash.flattendeep@^4.4.0:
   resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
   integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=
 
-lodash.isplainobject@^4.0.6:
-  version "4.0.6"
-  resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
-  integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
-
 lodash@^4.17.11, lodash@^4.17.5:
-  version "4.17.11"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
-  integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
+  version "4.17.15"
+  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
+  integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
 
 log-driver@^1.2.7:
   version "1.2.7"
@@ -1869,7 +1837,7 @@ rxjs@^6.4.0:
   dependencies:
     tslib "^1.9.0"
 
-safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.1:
+safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.1:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
   integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==


Test patch:
diff --git a/test/Error.spec.js b/test/Error.spec.js
index 91b7ad9b..e8947e4e 100644
--- a/test/Error.spec.js
+++ b/test/Error.spec.js
@@ -53,5 +53,49 @@ describe('Error', () => {
       });
       expect(e).to.have.property('statusCode', 400);
     });
+
+    it('can be extended via .extend method', () => {
+      const Custom = Error.extend({type: 'MyCustomErrorType'});
+      const err = new Custom({message: 'byaka'});
+      expect(err).to.be.instanceOf(Error.StripeError);
+      expect(err).to.have.property('type', 'MyCustomErrorType');
+      expect(err).to.have.property('name', 'MyCustomErrorType');
+      expect(err).to.have.property('message', 'byaka');
+    });
+
+    it('can create custom error via `extend` export', () => {
+      const Custom = Error.extend({
+        type: 'MyCardError',
+        populate(raw) {
+          this.detail = 'hello';
+          this.customField = 'hi';
+        },
+      });
+      const err = new Custom({
+        message: 'ee',
+      });
+      expect(err).to.be.instanceOf(Error.StripeError);
+      expect(err).to.have.property('type', 'MyCardError');
+      expect(err).to.have.property('name', 'MyCardError');
+      expect(err).to.have.property('message', 'ee');
+      expect(err).to.have.property('detail', 'hello');
+      expect(err).to.have.property('customField', 'hi');
+    });
+
+    it('ignores invalid constructor parameters for StripeError', () => {
+      const a = new Error.StripeError(false, 'a string');
+      expect(a).to.be.instanceOf(Error.StripeError);
+      expect(a).to.have.property('type', 'StripeError');
+      expect(a).to.have.property('message', '');
+
+      const b = new Error.StripeError('a string');
+      expect(b).to.be.instanceOf(Error.StripeError);
+      expect(b).to.have.property('type', 'StripeError');
+      expect(b).to.have.property('message', '');
+
+      const c = new Error.StripeError({some: 'object'}, {another: 'object'});
+      expect(c).to.be.instanceOf(Error.StripeError);
+      expect(c).to.have.property('type', 'StripeError');
+    });
   });
 });
diff --git a/test/StripeResource.spec.js b/test/StripeResource.spec.js
index 7fac0c5c..6aea57b8 100644
--- a/test/StripeResource.spec.js
+++ b/test/StripeResource.spec.js
@@ -1,7 +1,6 @@
 'use strict';
 
 const utils = require('../testUtils');
-const uuid = require('uuid/v4');
 
 const nock = require('nock');
 
@@ -17,13 +16,13 @@ describe('StripeResource', () => {
     });
   });
 
-  describe('_defaultHeaders', () => {
+  describe('_makeHeaders', () => {
     it('sets the Authorization header with Bearer auth using the global API key', () => {
-      const headers = stripe.invoices._defaultHeaders(null, 0, null);
+      const headers = stripe.invoices._makeHeaders(null, 0, null);
       expect(headers.Authorization).to.equal('Bearer fakeAuthToken');
     });
     it('sets the Authorization header with Bearer auth using the specified API key', () => {
-      const headers = stripe.invoices._defaultHeaders(
+      const headers = stripe.invoices._makeHeaders(
         'anotherFakeAuthToken',
         0,
         null
@@ -31,11 +30,11 @@ describe('StripeResource', () => {
       expect(headers.Authorization).to.equal('Bearer anotherFakeAuthToken');
     });
     it('sets the Stripe-Version header if an API version is provided', () => {
-      const headers = stripe.invoices._defaultHeaders(null, 0, '1970-01-01');
+      const headers = stripe.invoices._makeHeaders(null, 0, '1970-01-01');
       expect(headers['Stripe-Version']).to.equal('1970-01-01');
     });
     it('does not the set the Stripe-Version header if no API version is provided', () => {
-      const headers = stripe.invoices._defaultHeaders(null, 0, null);
+      const headers = stripe.invoices._makeHeaders(null, 0, null);
       expect(headers).to.not.include.keys('Stripe-Version');
     });
   });
@@ -67,7 +66,7 @@ describe('StripeResource', () => {
           .reply(200, '{}');
 
         realStripe.invoices.retrieveUpcoming(options.data, (err, response) => {
-          done();
+          done(err);
           scope.done();
         });
       });
@@ -92,7 +91,7 @@ describe('StripeResource', () => {
           'sub_123',
           options.data,
           (err, response) => {
-            done();
+            done(err);
             scope.done();
           }
         );
@@ -175,7 +174,7 @@ describe('StripeResource', () => {
 
         realStripe.charges.create(options.data, (err, charge) => {
           expect(charge.id).to.equal('ch_123');
-          done();
+          done(err);
         });
       });
 
@@ -198,7 +197,7 @@ describe('StripeResource', () => {
 
         realStripe.charges.create(options.data, (err, charge) => {
           expect(charge.id).to.equal('ch_123');
-          done();
+          done(err);
         });
       });
 
@@ -272,7 +271,7 @@ describe('StripeResource', () => {
 
         realStripe.charges.create(options.data, (err, charge) => {
           expect(charge.id).to.equal('ch_123');
-          done();
+          done(err);
         });
       });
 
@@ -295,7 +294,7 @@ describe('StripeResource', () => {
 
         realStripe.charges.retrieve('ch_123', (err, charge) => {
           expect(charge.id).to.equal('ch_123');
-          done();
+          done(err);
         });
       });
 
@@ -322,9 +321,9 @@ describe('StripeResource', () => {
 
         realStripe.setMaxNetworkRetries(1);
 
-        realStripe.charges.create(options.data, () => {
+        realStripe.charges.create(options.data, (err) => {
           expect(headers).to.have.property('idempotency-key');
-          done();
+          done(err);
         });
       });
 
@@ -357,7 +356,7 @@ describe('StripeResource', () => {
       });
 
       it('should reuse the given idempotency key provided for retries', (done) => {
-        const key = uuid();
+        const key = 'i-am-a-key';
         let headers;
 
         nock(`https://${options.host}`)
diff --git a/test/Webhook.spec.js b/test/Webhook.spec.js
index f1f1c089..2a3a9d71 100644
--- a/test/Webhook.spec.js
+++ b/test/Webhook.spec.js
@@ -2,7 +2,6 @@
 
 const stripe = require('../testUtils').getSpyableStripe();
 const expect = require('chai').expect;
-const Buffer = require('safe-buffer').Buffer;
 
 const EVENT_PAYLOAD = {
   id: 'evt_test_webhook',
diff --git a/test/flows.spec.js b/test/flows.spec.js
index 28a98be4..65d70f45 100644
--- a/test/flows.spec.js
+++ b/test/flows.spec.js
@@ -326,8 +326,6 @@ describe('Flows', function() {
         .slice(2);
 
       function onRequest(request) {
-        stripe.off('request', onRequest);
-
         expect(request).to.eql({
           api_version: 'latest',
           idempotency_key: idempotencyKey,
@@ -339,7 +337,7 @@ describe('Flows', function() {
         done();
       }
 
-      stripe.on('request', onRequest);
+      stripe.once('request', onRequest);
 
       stripe.charges
         .create(
@@ -411,7 +409,7 @@ describe('Flows', function() {
         done(new Error('How did you get here?'));
       }
 
-      stripe.on('response', onResponse);
+      stripe.once('response', onResponse);
       stripe.off('response', onResponse);
 
       stripe.charges
diff --git a/test/resources/BalanceTransactions.spec.js b/test/resources/BalanceTransactions.spec.js
index 72ede8cc..05b569ba 100644
--- a/test/resources/BalanceTransactions.spec.js
+++ b/test/resources/BalanceTransactions.spec.js
@@ -9,7 +9,7 @@ describe('BalanceTransactions Resource', function() {
       stripe.balanceTransactions.retrieve('txn_123');
       expect(stripe.LAST_REQUEST).to.deep.equal({
         method: 'GET',
-        url: '/v1/balance/history/txn_123',
+        url: '/v1/balance_transactions/txn_123',
         headers: {},
         data: {},
       });
@@ -21,7 +21,7 @@ describe('BalanceTransactions Resource', function() {
       stripe.balanceTransactions.list();
       expect(stripe.LAST_REQUEST).to.deep.equal({
         method: 'GET',
-        url: '/v1/balance/history',
+        url: '/v1/balance_transactions',
         headers: {},
         data: {},
       });
diff --git a/test/utils.spec.js b/test/utils.spec.js
index 8f8fd8ab..4156e724 100644
--- a/test/utils.spec.js
+++ b/test/utils.spec.js
@@ -4,7 +4,6 @@ require('../testUtils');
 
 const utils = require('../lib/utils');
 const expect = require('chai').expect;
-const Buffer = require('safe-buffer').Buffer;
 
 describe('utils', () => {
   describe('makeURLInterpolator', () => {
@@ -313,10 +312,10 @@ describe('utils', () => {
     });
   });
 
-  describe('removeEmpty', () => {
+  describe('removeNullish', () => {
     it('removes empty properties and leaves non-empty ones', () => {
       expect(
-        utils.removeEmpty({
+        utils.removeNullish({
           cat: 3,
           dog: false,
           rabbit: undefined,
@@ -328,9 +327,9 @@ describe('utils', () => {
       });
     });
 
-    it('throws an error if not given two things to compare', () => {
+    it('throws an error if not given an object', () => {
       expect(() => {
-        utils.removeEmpty('potato');
+        utils.removeNullish('potato');
       }).to.throw();
     });
   });