diff --git a/.changeset/tangy-bees-follow.md b/.changeset/tangy-bees-follow.md new file mode 100644 index 00000000000..2f0b945d942 --- /dev/null +++ b/.changeset/tangy-bees-follow.md @@ -0,0 +1,5 @@ +--- +'@clerk/shared': minor +--- + +Improve error handling when loading clerk-js. diff --git a/packages/shared/src/__tests__/loadClerkJsScript.test.ts b/packages/shared/src/__tests__/loadClerkJsScript.test.ts index bc8b58824ae..d135708634d 100644 --- a/packages/shared/src/__tests__/loadClerkJsScript.test.ts +++ b/packages/shared/src/__tests__/loadClerkJsScript.test.ts @@ -1,3 +1,4 @@ +import { ClerkRuntimeError } from '../error'; import { buildClerkJsScriptAttributes, clerkJsScriptUrl, @@ -86,8 +87,8 @@ describe('loadClerkJsScript(options)', () => { rejectedWith = error; } - expect(rejectedWith).toBeInstanceOf(Error); - expect(rejectedWith.message).toBe('Clerk: Failed to load Clerk'); + expect(rejectedWith).toBeInstanceOf(ClerkRuntimeError); + expect(rejectedWith.message).toContain('Clerk: Failed to load Clerk'); expect((window as any).Clerk).toBeUndefined(); }); @@ -137,8 +138,8 @@ describe('loadClerkJsScript(options)', () => { await loadPromise; fail('Should have thrown error'); } catch (error) { - expect(error).toBeInstanceOf(Error); - expect((error as Error).message).toBe('Clerk: Failed to load Clerk'); + expect(error).toBeInstanceOf(ClerkRuntimeError); + expect((error as Error).message).toContain('Clerk: Failed to load Clerk'); // The malformed Clerk object should still be there since it was set expect((window as any).Clerk).toEqual({ status: 'ready' }); } diff --git a/packages/shared/src/errors/runtimeError.ts b/packages/shared/src/errors/runtimeError.ts index 643f3f63df9..3c341c78edf 100644 --- a/packages/shared/src/errors/runtimeError.ts +++ b/packages/shared/src/errors/runtimeError.ts @@ -18,7 +18,12 @@ export class ClerkRuntimeError extends Error { */ code: string; - constructor(message: string, { code }: { code: string }) { + /** + * The original error that was caught to throw an instance of ClerkRuntimeError. + */ + cause?: Error; + + constructor(message: string, { code, cause }: { code: string; cause?: Error }) { const prefix = '🔒 Clerk:'; const regex = new RegExp(prefix.replace(' ', '\\s*'), 'i'); const sanitized = message.replace(regex, ''); @@ -27,6 +32,7 @@ export class ClerkRuntimeError extends Error { Object.setPrototypeOf(this, ClerkRuntimeError.prototype); + this.cause = cause; this.code = code; this.message = _message; this.clerkRuntimeError = true; diff --git a/packages/shared/src/loadClerkJsScript.ts b/packages/shared/src/loadClerkJsScript.ts index 68f81af1778..1cb78a1e68c 100644 --- a/packages/shared/src/loadClerkJsScript.ts +++ b/packages/shared/src/loadClerkJsScript.ts @@ -1,13 +1,15 @@ import type { ClerkOptions, SDKMetadata, Without } from '@clerk/types'; -import { buildErrorThrower } from './error'; +import { buildErrorThrower, ClerkRuntimeError } from './error'; import { createDevOrStagingUrlCache, parsePublishableKey } from './keys'; import { loadScript } from './loadScript'; import { isValidProxyUrl, proxyUrlToAbsoluteURL } from './proxy'; import { addClerkPrefix } from './url'; import { versionSelector } from './versionSelector'; -const FAILED_TO_LOAD_ERROR = 'Clerk: Failed to load Clerk'; +const ERROR_CODE = 'failed_to_load_clerk_js'; +const ERROR_CODE_TIMEOUT = 'failed_to_load_clerk_js_timeout'; +const FAILED_TO_LOAD_ERROR = 'Failed to load Clerk'; const { isDevOrStagingUrl } = createDevOrStagingUrlCache(); @@ -96,7 +98,7 @@ function waitForClerkWithTimeout(timeoutMs: number): Promise { - throw new Error(FAILED_TO_LOAD_ERROR); + }).catch(error => { + throw new ClerkRuntimeError(FAILED_TO_LOAD_ERROR + (error.message ? `, ${error.message}` : ''), { + code: ERROR_CODE, + cause: error, + }); }); return loadPromise; diff --git a/packages/shared/src/loadScript.ts b/packages/shared/src/loadScript.ts index ae5ea5859b0..e81f8730ef2 100644 --- a/packages/shared/src/loadScript.ts +++ b/packages/shared/src/loadScript.ts @@ -21,7 +21,7 @@ export async function loadScript(src = '', opts: LoadScriptOptions): Promise { + script.addEventListener('error', event => { script.remove(); - reject(); + reject(event.error ?? new Error(`failed to load script: ${src}`)); }); script.src = src;