Repository: stripe/stripe-node
Pull request #853 (2020-03-27 21:03:42)

Clean up StripeError.generate()
r? @richardm-stripe 
cc @ob-stripe (especially on `doc_url` which I am adding here – should we add that to all libs?)
cc @stripe/api-libraries 
cc @cah-dunn

This addresses https://github.com/stripe/stripe-node/issues/809 and a few related issues I noticed with our handling of errors and their types. 

1. Fix types for the `StripeError.generate()` static method, which returns the appropriate subclass based on a `type` string (and is appropriately typed as such). 
2. Add types for the `StripeError` constructor. 
3. Pull out the types for Errors into their own definition file.
4. Make `generate` available at the top-level, so you can do `Stripe.errors.generate({})` which I think is much prettier than `Stripe.errors.StripeError.generate({})`. Feedback welcome on whether this is a good idea or not.
5. Add types for `StripeInvalidGrantError`, which I think we had forgotten to add types for before.
6. Add support for `authentication_error` and `rate_limit_error` in `.generate()`.

One tradeoff I made which I don't feel great about is that the constructor/generate is now typed more loosely than the object itself, so for example if you `new StripeError()` it will have `undefined` values for `rawType`, `requestId`, and `headers`, even though all of those are typed as being required fields on `StripeError` instances. 

We could either:
1) Set these to defaults, such as `'unknown'`, `''`, `{}`.
2) Change the types to be possibly undefined.
3) In a future major version, throw an error if these fields are instantiated as undefined.
4) Shrug – it's useful to have the types for the typical case of when the API is returning things (and we ~know they'll be there), and if developers are mocking things in tests, it's not too surprising that something is undefined even though TS said it wouldn't be. Plus, who looks there?

I'm leaning towards (2) or (3+4) and would value input from the community and from fellow Stripes.


ob-stripe: > cc @ob-stripe (especially on `doc_url` which I am adding here – should we add that to all libs?)

Most of them should have it already, and those that don't, should :)

richardm-stripe: LGTM. I like option 3, and 4 in the meantime.
rattrayalex-stripe: Great – I'll wait a bit to see if @cah-dunn has any feedback, and otherwise ship.
cah-dunn: @rattrayalex-stripe This looks great. Thanks for all of the fixes and QoL changes!

Patch:
diff --git a/lib/Error.js b/lib/Error.js
index f0f318c7..e038a4f7 100644
--- a/lib/Error.js
+++ b/lib/Error.js
@@ -12,6 +12,7 @@ class StripeError extends Error {
     this.raw = raw;
     this.rawType = raw.type;
     this.code = raw.code;
+    this.doc_url = raw.doc_url;
     this.param = raw.param;
     this.detail = raw.detail;
     this.headers = raw.headers;
@@ -38,6 +39,10 @@ class StripeError extends Error {
         return new StripeInvalidRequestError(rawStripeError);
       case 'api_error':
         return new StripeAPIError(rawStripeError);
+      case 'authentication_error':
+        return new StripeAuthenticationError(rawStripeError);
+      case 'rate_limit_error':
+        return new StripeRateLimitError(rawStripeError);
       case 'idempotency_error':
         return new StripeIdempotencyError(rawStripeError);
       case 'invalid_grant':
@@ -116,6 +121,7 @@ class StripeIdempotencyError extends StripeError {}
  */
 class StripeInvalidGrantError extends StripeError {}
 
+module.exports.generate = StripeError.generate;
 module.exports.StripeError = StripeError;
 module.exports.StripeCardError = StripeCardError;
 module.exports.StripeInvalidRequestError = StripeInvalidRequestError;
diff --git a/types/2020-03-02/index.d.ts b/types/2020-03-02/index.d.ts
index e7c57aba..29b0239b 100644
--- a/types/2020-03-02/index.d.ts
+++ b/types/2020-03-02/index.d.ts
@@ -1,7 +1,8 @@
 ///<reference path='../lib.d.ts' />
 ///<reference path='../shared.d.ts' />
-///<reference path='../Webhooks.d.ts' />
+///<reference path='../Errors.d.ts' />
 ///<reference path='../OAuth.d.ts' />
+///<reference path='../Webhooks.d.ts' />
 ///<reference path='./Accounts.d.ts' />
 ///<reference path='./AccountLinks.d.ts' />
 ///<reference path='./AlipayAccounts.d.ts' />
diff --git a/types/Errors.d.ts b/types/Errors.d.ts
new file mode 100644
index 00000000..fe063ffd
--- /dev/null
+++ b/types/Errors.d.ts
@@ -0,0 +1,229 @@
+declare module 'stripe' {
+  namespace Stripe {
+    export type Errors = {
+      generate: typeof StripeError.generate;
+      StripeError: typeof StripeError;
+      StripeCardError: typeof StripeCardError;
+      StripeInvalidRequestError: typeof StripeInvalidRequestError;
+      StripeAPIError: typeof StripeAPIError;
+      StripeAuthenticationError: typeof StripeAuthenticationError;
+      StripePermissionError: typeof StripePermissionError;
+      StripeRateLimitError: typeof StripeRateLimitError;
+      StripeConnectionError: typeof StripeConnectionError;
+      StripeSignatureVerificationError: typeof StripeSignatureVerificationError;
+      StripeIdempotencyError: typeof StripeIdempotencyError;
+      StripeInvalidGrantError: typeof StripeInvalidGrantError;
+    };
+
+    export type RawErrorType =
+      | 'card_error'
+      | 'invalid_request_error'
+      | 'api_error'
+      | 'idempotency_error'
+      | 'rate_limit_error'
+      | 'authentication_error'
+      | 'invalid_grant';
+
+    type StripeRawError = {
+      message?: string;
+
+      type: RawErrorType;
+
+      headers?: {[header: string]: string};
+      statusCode?: number;
+      requestId?: string;
+
+      code?: string;
+      doc_code?: string;
+      decline_code?: string;
+      param?: string;
+      detail?: string;
+
+      charge?: string;
+      payment_intent?: Stripe.PaymentIntent;
+      payment_method?: Stripe.PaymentMethod;
+      setup_intent?: Stripe.SetupIntent;
+      source?: Stripe.Source;
+    };
+
+    class StripeError extends Error {
+      constructor(rawError: StripeRawError);
+
+      static generate(
+        rawError: StripeRawError & {type: 'card_error'}
+      ): StripeCardError;
+      static generate(
+        rawError: StripeRawError & {type: 'invalid_request_error'}
+      ): StripeInvalidRequestError;
+      static generate(
+        rawError: StripeRawError & {type: 'api_error'}
+      ): StripeAPIError;
+      static generate(
+        rawError: StripeRawError & {type: 'authentication_error'}
+      ): StripeAuthenticationError;
+      static generate(
+        rawError: StripeRawError & {type: 'rate_limit_error'}
+      ): StripeRateLimitError;
+      static generate(
+        rawError: StripeRawError & {type: 'idempotency_error'}
+      ): StripeIdempotencyError;
+      static generate(
+        rawError: StripeRawError & {type: 'invalid_grant'}
+      ): StripeInvalidGrantError;
+      static generate(
+        rawError: StripeRawError & {type: RawErrorType}
+      ): StripeError;
+
+      /**
+       * A human-readable message giving more details about the error. For card errors, these messages can
+       * be shown to your users.
+       */
+      readonly message: string;
+
+      readonly type: keyof Errors;
+
+      /**
+       * See the "error types" section at https://stripe.com/docs/api/errors
+       */
+      readonly rawType: RawErrorType;
+
+      /**
+       * For card errors, a short string describing the kind of card error that occurred.
+       *
+       * @docs https://stripe.com/docs/error-codes
+       */
+      readonly code?: string;
+
+      /**
+       * A URL to more information about the error code reported.
+       *
+       * @docs https://stripe.com/docs/error-codes
+       */
+      readonly doc_url?: string;
+
+      /**
+       * Typically a 4xx or 5xx.
+       */
+      readonly statusCode?: number;
+
+      readonly raw: unknown;
+
+      readonly headers: {
+        [key: string]: string;
+      };
+
+      readonly requestId: string;
+
+      /**
+       * The parameter the error relates to if the error is parameter-specific. You can use this to display a
+       * message near the correct form field, for example.
+       */
+      readonly param?: string;
+
+      /** @deprecated */
+      readonly detail?: unknown;
+
+      readonly charge?: string;
+      readonly decline_code?: string;
+      readonly payment_intent?: PaymentIntent;
+      readonly payment_method?: PaymentMethod;
+      readonly setup_intent?: SetupIntent;
+      readonly source?: Source;
+    }
+
+    /**
+     * Card errors are the most common type of error you should expect to handle.
+     * They result when the user enters a card that can't be charged for some reason.
+     */
+    class StripeCardError extends StripeError {
+      readonly type: 'StripeCardError';
+      readonly rawType: 'card_error';
+
+      /**
+       * @docs https://stripe.com/docs/declines/codes
+       */
+      readonly decline_code: string;
+    }
+
+    /**
+     * Invalid request errors arise when your request has invalid parameters.
+     */
+    class StripeInvalidRequestError extends StripeError {
+      readonly type: 'StripeInvalidRequestError';
+      readonly rawType: 'invalid_request_error';
+    }
+
+    /**
+     * API errors cover any other type of problem (e.g., a temporary problem with Stripe's servers),
+     * and are extremely uncommon.
+     *
+     * It could also be raised in the case that a new error has been introduced in the API,
+     * but this version of the library doesn't know how to handle it.
+     */
+    class StripeAPIError extends StripeError {
+      readonly type: 'StripeAPIError';
+      readonly rawType: 'api_error';
+    }
+
+    /**
+     * Failure to properly authenticate yourself in the request.
+     */
+    class StripeAuthenticationError extends StripeError {
+      readonly type: 'StripeAuthenticationError';
+      readonly rawType: 'authentication_error';
+    }
+
+    /**
+     * Access was attempted on a resource that wasn't allowed.
+     */
+    class StripePermissionError extends StripeError {
+      readonly type: 'StripePermissionError';
+    }
+
+    /**
+     * Too many requests hit the API too quickly.
+     * @docs https://stripe.com/docs/rate-limits
+     */
+    class StripeRateLimitError extends StripeError {
+      readonly type: 'StripeRateLimitError';
+      readonly rawType: 'rate_limit_error';
+    }
+
+    /**
+     * The library cannot connect to Stripe.
+     * This can happen for a variety of reasons,
+     * such as loss of network connectivity or a bad TLS certificate.
+     */
+    class StripeConnectionError extends StripeError {
+      readonly type: 'StripeConnectionError';
+    }
+
+    /**
+     * The signature verification for a webhook failed.
+     * @docs https://stripe.com/docs/webhooks/signatures
+     */
+    class StripeSignatureVerificationError extends StripeError {
+      readonly type: 'StripeSignatureVerificationError';
+    }
+
+    /**
+     * Idempotency errors occur when an `Idempotency-Key` is re-used on a request that does not match the first request's API endpoint and parameters.
+     * @docs https://stripe.com/docs/api/idempotent_requests?lang=node
+     */
+    class StripeIdempotencyError extends StripeError {
+      readonly type: 'StripeIdempotencyError';
+      readonly rawType: 'idempotency_error';
+    }
+
+    /**
+     * 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 {
+      readonly type: 'StripeInvalidGrantError';
+      readonly rawType: 'invalid_grant';
+    }
+  }
+}
diff --git a/types/lib.d.ts b/types/lib.d.ts
index f1e2e2d3..75840de5 100644
--- a/types/lib.d.ts
+++ b/types/lib.d.ts
@@ -180,164 +180,5 @@ declare module 'stripe' {
       url?: string;
       version?: string;
     }
-
-    export type Errors = {
-      StripeError: typeof StripeError;
-      StripeCardError: typeof StripeCardError;
-      StripeInvalidRequestError: typeof StripeInvalidRequestError;
-      StripeAPIError: typeof StripeAPIError;
-      StripeAuthenticationError: typeof StripeAuthenticationError;
-      StripePermissionError: typeof StripePermissionError;
-      StripeRateLimitError: typeof StripeRateLimitError;
-      StripeConnectionError: typeof StripeConnectionError;
-      StripeSignatureVerificationError: typeof StripeSignatureVerificationError;
-      StripeIdempotencyError: typeof StripeIdempotencyError;
-    };
-
-    export type RawErrorType =
-      | 'card_error'
-      | 'invalid_request_error'
-      | 'api_error'
-      | 'idempotency_error'
-      | 'rate_limit_error'
-      | 'authentication_error'
-      | 'invalid_grant';
-
-    class StripeError extends Error {
-      static populate(type: RawErrorType): StripeError;
-
-      /**
-       * A human-readable message giving more details about the error. For card errors, these messages can
-       * be shown to your users.
-       */
-      readonly message: string;
-
-      readonly type: keyof Errors;
-
-      /**
-       * See the "error types" section at https://stripe.com/docs/api/errors
-       */
-      readonly rawType: RawErrorType;
-
-      /**
-       * For card errors, a short string describing the kind of card error that occurred.
-       *
-       * @docs https://stripe.com/docs/error-codes
-       */
-      readonly code?: string;
-
-      /**
-       * Typically a 4xx or 5xx.
-       */
-      readonly statusCode?: number;
-
-      readonly raw: unknown;
-
-      readonly headers: {
-        [key: string]: string;
-      };
-
-      readonly requestId: string;
-
-      /**
-       * The parameter the error relates to if the error is parameter-specific. You can use this to display a
-       * message near the correct form field, for example.
-       */
-      readonly param?: string;
-
-      /** @deprecated */
-      readonly detail?: unknown;
-
-      readonly charge?: string;
-      readonly decline_code?: string;
-      readonly payment_intent?: PaymentIntent;
-      readonly payment_method?: PaymentMethod;
-      readonly setup_intent?: SetupIntent;
-      readonly source?: Source;
-    }
-
-    /**
-     * Card errors are the most common type of error you should expect to handle.
-     * They result when the user enters a card that can't be charged for some reason.
-     */
-    class StripeCardError extends StripeError {
-      readonly type: 'StripeCardError';
-      readonly rawType: 'card_error';
-
-      /**
-       * @docs https://stripe.com/docs/declines/codes
-       */
-      readonly decline_code: string;
-    }
-
-    /**
-     * Invalid request errors arise when your request has invalid parameters.
-     */
-    class StripeInvalidRequestError extends StripeError {
-      readonly type: 'StripeInvalidRequestError';
-      readonly rawType: 'invalid_request_error';
-    }
-
-    /**
-     * API errors cover any other type of problem (e.g., a temporary problem with Stripe's servers),
-     * and are extremely uncommon.
-     *
-     * It could also be raised in the case that a new error has been introduced in the API,
-     * but this version of the library doesn't know how to handle it.
-     */
-    class StripeAPIError extends StripeError {
-      readonly type: 'StripeAPIError';
-      readonly rawType: 'api_error';
-    }
-
-    /**
-     * Failure to properly authenticate yourself in the request.
-     */
-    class StripeAuthenticationError extends StripeError {
-      readonly type: 'StripeAuthenticationError';
-      readonly rawType: 'authentication_error';
-    }
-
-    /**
-     * Access was attempted on a resource that wasn't allowed.
-     */
-    class StripePermissionError extends StripeError {
-      readonly type: 'StripePermissionError';
-    }
-
-    /**
-     * Too many requests hit the API too quickly.
-     * @docs https://stripe.com/docs/rate-limits
-     */
-    class StripeRateLimitError extends StripeError {
-      readonly type: 'StripeRateLimitError';
-      readonly rawType: 'rate_limit_error';
-    }
-
-    /**
-     * The library cannot connect to Stripe.
-     * This can happen for a variety of reasons,
-     * such as loss of network connectivity or a bad TLS certificate.
-     */
-    class StripeConnectionError extends StripeError {
-      readonly type: 'StripeConnectionError';
-    }
-
-    /**
-     * The signature verification for a webhook failed.
-     * @docs https://stripe.com/docs/webhooks/signatures
-     */
-    class StripeSignatureVerificationError extends StripeError {
-      readonly type: 'StripeSignatureVerificationError';
-    }
-
-    /**
-     * Idempotency errors occur when an `Idempotency-Key` is re-used on a request that does not match the first request's API endpoint and parameters.
-     * @docs https://stripe.com/docs/api/idempotent_requests?lang=node
-     */
-    class StripeIdempotencyError extends StripeError {
-      readonly type: 'StripeIdempotencyError';
-      readonly rawType: 'idempotency_error';
-    }
   }
 }


Test patch:
diff --git a/types/test/typescriptTest.ts b/types/test/typescriptTest.ts
index f772b469..acb7799a 100644
--- a/types/test/typescriptTest.ts
+++ b/types/test/typescriptTest.ts
@@ -132,3 +132,9 @@ stripe.setHost('host', 'port', 'protocol');
     }
   }
 })();
+
+const stripeCardError: Stripe.StripeCardError = Stripe.errors.generate({
+  type: 'card_error',
+  code: 'card_declined',
+  charge: 'ch_123',
+});