From 205fb18785a7b133cb7ec0f4c4d3cc6c9343ff8f Mon Sep 17 00:00:00 2001 From: Harvmaster Date: Thu, 23 Jul 2026 07:56:50 +0000 Subject: [PATCH] Use Custom Error Classes for Validation --- source/errors.ts | 40 +++++++++++++++ source/exponential-backoff.ts | 86 +++++++++++++++++++------------- test/exponential-backoff.test.ts | 26 ++++++++-- 3 files changed, 112 insertions(+), 40 deletions(-) diff --git a/source/errors.ts b/source/errors.ts index 02b32f7..be4d05b 100644 --- a/source/errors.ts +++ b/source/errors.ts @@ -20,3 +20,43 @@ export class ExponentialBackoffStoppedRetriesError extends Error { this.name = 'ExponentialBackoffStoppedRetriesError'; } } + +/** + * Error thrown when an exponential backoff option is too small + */ +export class ExponentialBackoffNumberTooSmallError extends Error { + constructor(option: string, value: number, min: number) { + super(`Exponential backoff option "${option}" is too small. Must be at least ${min}`); + this.name = 'ExponentialBackoffNumberTooSmallError'; + } +} + +/** + * Error thrown when an exponential backoff option is out of bounds + */ +export class ExponentialBackoffNumberOutOfBoundsError extends Error { + constructor(option: string, value: number, min: number, max: number) { + super(`Exponential backoff option "${option}" is out of bounds. Must be between ${min} and ${max}`); + this.name = 'ExponentialBackoffNumberOutOfBoundsError'; + } +} + +/** + * Error thrown when an exponential backoff option is an invalid infinite integer + */ +export class ExponentialBackoffInvalidInfiniteIntegerError extends Error { + constructor(option: string) { + super(`Exponential backoff option "${option}" is invalid. Must be a finite number`); + this.name = 'ExponentialBackoffInvalidInfiniteIntegerError'; + } +} + +/** + * Error thrown when an exponential backoff option is not an integer + */ +export class ExponentialBackoffNonIntegerError extends Error { + constructor(option: string) { + super(`Exponential backoff option "${option}" is invalid. Must be an integer`); + this.name = 'ExponentialBackoffNonIntegerError'; + } +} diff --git a/source/exponential-backoff.ts b/source/exponential-backoff.ts index 8b7b8a8..563d35e 100644 --- a/source/exponential-backoff.ts +++ b/source/exponential-backoff.ts @@ -1,4 +1,11 @@ -import { ExponentialBackoffStoppedRetriesError, ExponentialBackoffMaxRetriesHitError } from './errors.ts'; +import { + ExponentialBackoffStoppedRetriesError, + ExponentialBackoffMaxRetriesHitError, + ExponentialBackoffInvalidInfiniteIntegerError, + ExponentialBackoffNonIntegerError, + ExponentialBackoffNumberTooSmallError, + ExponentialBackoffNumberOutOfBoundsError, +} from './errors.ts'; /** * Exponential backoff is a technique used to retry a function after a delay. @@ -106,50 +113,57 @@ export class ExponentialBackoff { * @throws An error if the options are invalid */ public static validateOptions(options: ExponentialBackoffOptions): void { - // Validate the max delay is a finite number not less than 0 - if (!Number.isFinite(options.maxDelay)) { - throw new Error('maxDelay must be a finite number'); - } + /** Validate the value is finite, throwing an {@link ExponentialBackoffInvalidInfiniteIntegerError} if the value is infinite */ + const isFinite = (key: string, value: number): void => { + if (!Number.isFinite(value)) { + throw new ExponentialBackoffInvalidInfiniteIntegerError(key); + } + }; - if (options.maxDelay < 0) { - throw new Error('maxDelay must be not less than 0'); - } + /** Validate the value is an integer, throwing a {@link ExponentialBackoffNonIntegerError} if it is not an integer */ + const isInteger = (key: string, value: number): void => { + if (!Number.isInteger(value)) { + throw new ExponentialBackoffNonIntegerError(key); + } + }; - // Validate the max attempts is a finite number not less than 0 - if (!Number.isFinite(options.maxAttempts)) { - throw new Error('maxAttempts must be a finite number'); - } + /** Validate the value is within the bounds, throwing a {@link ExponentialBackoffNumberOutOfBoundsError} if it is not within the bounds */ + const isWithinBounds = (key: string, value: number, min: number, max?: number): void => { + // If both the min and max are defined, validate the value, throwing a number out of bounds error if it is not within the bounds + if (min !== undefined && max !== undefined) { + if (value < min || value > max) { + throw new ExponentialBackoffNumberOutOfBoundsError(key, value, min, max); + } - if (options.maxAttempts < 0) { - throw new Error('maxAttempts must be not less than 0'); - } + return; + } - // Validate the base delay is a finite number not less than 0 - if (!Number.isFinite(options.baseDelay)) { - throw new Error('baseDelay must be a finite number'); - } + // If only the min is defined, validate the value, throwing a number too small error if it is less than the min + if (value < min) { + throw new ExponentialBackoffNumberTooSmallError(key, value, min); + } + }; - if (options.baseDelay < 0) { - throw new Error('baseDelay must be not less than 0'); - } + // Validate the max delay + isFinite('maxDelay', options.maxDelay); + isWithinBounds('maxDelay', options.maxDelay, 0); - // Validate the growth rate is a finite number not less than 0 - if (!Number.isFinite(options.growthRate)) { - throw new Error('growthRate must be a finite number'); - } + // Validate the max attempts + isFinite('maxAttempts', options.maxAttempts); + isInteger('maxAttempts', options.maxAttempts); + isWithinBounds('maxAttempts', options.maxAttempts, 0); - if (options.growthRate < 0) { - throw new Error('growthRate must be not less than 0'); - } + // Validate the base delay + isFinite('baseDelay', options.baseDelay); + isWithinBounds('baseDelay', options.baseDelay, 0); - // Validate the jitter is a finite number not less than 0 or greater than 1 - if (!Number.isFinite(options.jitter)) { - throw new Error('jitter must be a finite number'); - } + // Validate the growth rate + isFinite('growthRate', options.growthRate); + isWithinBounds('growthRate', options.growthRate, 0); - if (options.jitter < 0 || options.jitter > 1) { - throw new Error('jitter must be not less than 0 or greater than 1'); - } + // Validate the jitter + isFinite('jitter', options.jitter); + isWithinBounds('jitter', options.jitter, 0, 1); } /** diff --git a/test/exponential-backoff.test.ts b/test/exponential-backoff.test.ts index 7c29a96..556ef11 100644 --- a/test/exponential-backoff.test.ts +++ b/test/exponential-backoff.test.ts @@ -498,7 +498,7 @@ const testExponentialBackoffValidateOptionsRejectsNegativeValues = (): void => { ExponentialBackoff.validateOptions({ ...validExponentialBackoffOptions, [field]: value, - })).toThrow(`${field} must be not less than 0`); + })).toThrow(`Exponential backoff option "${field}" is too small. Must be at least 0`); } }; @@ -515,7 +515,7 @@ const testExponentialBackoffValidateOptionsRejectsInvalidJitter = (): void => { ExponentialBackoff.validateOptions({ ...validExponentialBackoffOptions, jitter: value, - })).toThrow('jitter must be not less than 0 or greater than 1'); + })).toThrow('Exponential backoff option "jitter" is out of bounds. Must be between 0 and 1'); } }; @@ -538,7 +538,24 @@ const testExponentialBackoffValidateOptionsRejectsNonFiniteValues = (): void => ExponentialBackoff.validateOptions({ ...validExponentialBackoffOptions, [field]: value, - })).toThrow(`${field} must be a finite number`); + })).toThrow(`Exponential backoff option "${field}" is invalid. Must be a finite number`); + } +}; + +/** + * Tests that {@link ExponentialBackoff.validateOptions} rejects non-integer values. + */ +const testExponentialBackoffValidateOptionsRejectsNonIntegerValues = (): void => { + // Define our test cases with each value being a non-integer + const nonIntegerCases = [{ field: 'maxAttempts', value: 1.5 }] as const; + + // Iterate through the test cases and expect an error to be thrown + for (const { field, value } of nonIntegerCases) { + expect(() => + ExponentialBackoff.validateOptions({ + ...validExponentialBackoffOptions, + [field]: value, + })).toThrow(`Exponential backoff option "${field}" is invalid. Must be an integer`); } }; @@ -561,7 +578,7 @@ const testExponentialBackoffValidateOptionsRejectsNaN = (): void => { ExponentialBackoff.validateOptions({ ...validExponentialBackoffOptions, [field]: value, - })).toThrow(`${field} must be a finite number`); + })).toThrow(`Exponential backoff option "${field}" is invalid. Must be a finite number`); } }; @@ -589,6 +606,7 @@ const runTests = async (): Promise => { test('ExponentialBackoff.validateOptions: rejects negative values', testExponentialBackoffValidateOptionsRejectsNegativeValues); test('ExponentialBackoff.validateOptions: rejects invalid jitter', testExponentialBackoffValidateOptionsRejectsInvalidJitter); test('ExponentialBackoff.validateOptions: rejects Infinity', testExponentialBackoffValidateOptionsRejectsNonFiniteValues); + test('ExponentialBackoff.validateOptions: rejects non-integer values', testExponentialBackoffValidateOptionsRejectsNonIntegerValues); test('ExponentialBackoff.validateOptions: rejects NaN', testExponentialBackoffValidateOptionsRejectsNaN); };