From 3aac5b0814a9b0da2e82d75d57c18ad6e845698c Mon Sep 17 00:00:00 2001 From: Kyle Scott Date: Mon, 12 Jan 2026 12:34:02 -0500 Subject: [PATCH 01/11] start build error handling --- packages/core/src/bin/commands/start.ts | 3 ++ packages/core/src/build/index.ts | 64 +++++----------------- packages/core/src/internal/errors.ts | 72 ++++++++++++------------- 3 files changed, 52 insertions(+), 87 deletions(-) diff --git a/packages/core/src/bin/commands/start.ts b/packages/core/src/bin/commands/start.ts index 5bffae936..c686acffe 100644 --- a/packages/core/src/bin/commands/start.ts +++ b/packages/core/src/bin/commands/start.ts @@ -225,6 +225,9 @@ export async function start({ preBuild: preCompileResult.result, schemaBuild: compileSchemaResult.result, }); + + // TODO(kyle) database diagnostic + const crashRecoveryCheckpoint = await database.migrate({ buildId: indexingBuildResult.result.buildId, chains: indexingBuildResult.result.chains, diff --git a/packages/core/src/build/index.ts b/packages/core/src/build/index.ts index db285ac0f..cd77546b1 100644 --- a/packages/core/src/build/index.ts +++ b/packages/core/src/build/index.ts @@ -183,6 +183,7 @@ export const createBuild = async ({ return { status: "success", exports } as const; } catch (error_) { const relativePath = path.relative(common.options.rootDir, file); + // TODO(kyle) error should be a `BuildError` const error = parseViteNodeError(relativePath, error_ as Error); return { status: "error", error } as const; } @@ -206,13 +207,13 @@ export const createBuild = async ({ ); }); - const res = await Promise.race([executeFile({ file }), timeout]); - if (res instanceof NonRetryableUserError) { - return { status: "error", error: res }; + const result = await Promise.race([executeFile({ file }), timeout]); + if (result instanceof NonRetryableUserError) { + return { status: "error", error: result }; } clearTimeout(timeoutId!); - return res; + return result; }; const build = { @@ -222,12 +223,6 @@ export const createBuild = async ({ }); if (executeResult.status === "error") { - common.logger.error({ - msg: "Error while executing file", - file: "ponder.config.ts", - error: executeResult.error, - }); - return executeResult; } @@ -255,12 +250,6 @@ export const createBuild = async ({ }); if (executeResult.status === "error") { - common.logger.error({ - msg: "Error while executing file", - file: "ponder.schema.ts", - error: executeResult.error, - }); - return executeResult; } @@ -284,11 +273,6 @@ export const createBuild = async ({ const executeResult = await executeFileWithTimeout({ file }); if (executeResult.status === "error") { - common.logger.error({ - msg: "Error while executing file", - file: path.relative(common.options.rootDir, file), - error: executeResult.error, - }); return executeResult; } } @@ -301,10 +285,7 @@ export const createBuild = async ({ const contents = fs.readFileSync(file, "utf-8"); hash.update(contents); } catch (e) { - common.logger.warn({ - msg: "Unable to read file", - file, - }); + common.logger.warn({ msg: "Unable to read file", file }); hash.update(file); } } @@ -325,26 +306,17 @@ export const createBuild = async ({ globalThis.PONDER_INDEXING_BUILD = configBuild; globalThis.PONDER_DATABASE = database; - if (!fs.existsSync(common.options.apiFile)) { + if (fs.existsSync(common.options.apiFile) === false) { const error = new BuildError( `API endpoint file not found. Create a file at ${common.options.apiFile}. Read more: https://ponder.sh/docs/api-reference/ponder/api-endpoints`, ); - error.stack = undefined; return { status: "error", error }; } - const executeResult = await executeFile({ - file: common.options.apiFile, - }); + const executeResult = await executeFile({ file: common.options.apiFile }); if (executeResult.status === "error") { - common.logger.error({ - msg: "Error while executing file", - file: path.relative(common.options.rootDir, common.options.apiFile), - error: executeResult.error, - }); - return executeResult; } @@ -354,7 +326,6 @@ export const createBuild = async ({ const error = new BuildError( "API endpoint file does not export a Hono instance as the default export. Read more: https://ponder.sh/docs/api-reference/ponder/api-endpoints", ); - error.stack = undefined; return { status: "error", error }; } @@ -372,7 +343,6 @@ export const createBuild = async ({ const error = new BuildError( `Database schema required. Specify with "DATABASE_SCHEMA" env var or "--schema" CLI flag. Read more: https://ponder.sh/docs/database#database-schema`, ); - error.stack = undefined; return { status: "error", error } as const; } @@ -385,7 +355,6 @@ export const createBuild = async ({ const error = new BuildError( "Views schema cannot be the same as the schema.", ); - error.stack = undefined; return { status: "error", error } as const; } @@ -393,7 +362,6 @@ export const createBuild = async ({ const error = new BuildError( `Invalid schema name. "ponder_sync" is a reserved schema name.`, ); - error.stack = undefined; return { status: "error", error } as const; } @@ -401,7 +369,6 @@ export const createBuild = async ({ const error = new BuildError( `Invalid views schema name. "ponder_sync" is a reserved schema name.`, ); - error.stack = undefined; return { status: "error", error } as const; } @@ -409,7 +376,6 @@ export const createBuild = async ({ const error = new BuildError( `Schema name cannot be longer than ${MAX_DATABASE_OBJECT_NAME_LENGTH} characters.`, ); - error.stack = undefined; return { status: "error", error } as const; } @@ -417,7 +383,6 @@ export const createBuild = async ({ const error = new BuildError( `Views schema name cannot be longer than ${MAX_DATABASE_OBJECT_NAME_LENGTH} characters.`, ); - error.stack = undefined; return { status: "error", error } as const; } @@ -535,13 +500,11 @@ export const createBuild = async ({ route.path === "/ready" || route.path === "/status" || route.path === "/metrics" || - route.path === "/health" || - route.path === "/client" + route.path === "/health" ) { const error = new BuildError( `Validation failed: API route "${route.path}" is reserved for internal use.`, ); - error.stack = undefined; return { status: "error", error } as const; } } @@ -691,9 +654,10 @@ export const createBuild = async ({ rpc_chain_id: hexToNumber(chainId), }); } - } catch (e) { - const error = new RetryableError("Failed to connect to JSON-RPC"); - error.stack = undefined; + } catch (_error) { + const error = new BuildError("Failed to connect to JSON-RPC", { + cause: _error as Error, + }); return { status: "error", error } as const; } @@ -733,7 +697,6 @@ export const createBuild = async ({ const error = new RetryableError( `Failed to connect to PGlite database. Please check your database connection settings.\n\n${(e as any).message}`, ); - error.stack = undefined; return { status: "error", error }; } finally { await driver.close(); @@ -768,7 +731,6 @@ export const createBuild = async ({ const error = new RetryableError( `Failed to connect to database. Please check your database connection settings.\n\n${(e as any).message}`, ); - error.stack = undefined; return { status: "error", error }; } finally { await pool.end(); diff --git a/packages/core/src/internal/errors.ts b/packages/core/src/internal/errors.ts index 1f50b8c86..3cdefd49d 100644 --- a/packages/core/src/internal/errors.ts +++ b/packages/core/src/internal/errors.ts @@ -33,8 +33,8 @@ export class RetryableError extends BaseError { export class ShutdownError extends NonRetryableUserError { override name = "ShutdownError"; - constructor(message?: string | undefined) { - super(message); + constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, ShutdownError.prototype); } } @@ -42,8 +42,8 @@ export class ShutdownError extends NonRetryableUserError { export class BuildError extends NonRetryableUserError { override name = "BuildError"; - constructor(message?: string | undefined) { - super(message); + constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, BuildError.prototype); } } @@ -51,8 +51,8 @@ export class BuildError extends NonRetryableUserError { export class MigrationError extends NonRetryableUserError { override name = "MigrationError"; - constructor(message?: string | undefined) { - super(message); + constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, MigrationError.prototype); } } @@ -62,8 +62,8 @@ export class MigrationError extends NonRetryableUserError { export class UniqueConstraintError extends NonRetryableUserError { override name = "UniqueConstraintError"; - constructor(message?: string | undefined) { - super(message); + constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, UniqueConstraintError.prototype); } } @@ -71,8 +71,8 @@ export class UniqueConstraintError extends NonRetryableUserError { export class NotNullConstraintError extends NonRetryableUserError { override name = "NotNullConstraintError"; - constructor(message?: string | undefined) { - super(message); + constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, NotNullConstraintError.prototype); } } @@ -80,8 +80,8 @@ export class NotNullConstraintError extends NonRetryableUserError { export class InvalidStoreAccessError extends NonRetryableUserError { override name = "InvalidStoreAccessError"; - constructor(message?: string | undefined) { - super(message); + constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, InvalidStoreAccessError.prototype); } } @@ -89,8 +89,8 @@ export class InvalidStoreAccessError extends NonRetryableUserError { export class RecordNotFoundError extends NonRetryableUserError { override name = "RecordNotFoundError"; - constructor(message?: string | undefined) { - super(message); + constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, RecordNotFoundError.prototype); } } @@ -98,8 +98,8 @@ export class RecordNotFoundError extends NonRetryableUserError { export class CheckConstraintError extends NonRetryableUserError { override name = "CheckConstraintError"; - constructor(message?: string | undefined) { - super(message); + constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, CheckConstraintError.prototype); } } @@ -109,8 +109,8 @@ export class CheckConstraintError extends NonRetryableUserError { export class DbConnectionError extends RetryableError { override name = "DbConnectionError"; - constructor(message?: string | undefined) { - super(message); + constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, DbConnectionError.prototype); } } @@ -118,8 +118,8 @@ export class DbConnectionError extends RetryableError { export class TransactionStatementError extends RetryableError { override name = "TransactionStatementError"; - constructor(message?: string | undefined) { - super(message); + constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, TransactionStatementError.prototype); } } @@ -127,8 +127,8 @@ export class TransactionStatementError extends RetryableError { export class CopyFlushError extends RetryableError { override name = "CopyFlushError"; - constructor(message?: string | undefined) { - super(message); + constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, CopyFlushError.prototype); } } @@ -150,8 +150,8 @@ export class InvalidEventAccessError extends RetryableError { export class InvalidStoreMethodError extends NonRetryableUserError { override name = "InvalidStoreMethodError"; - constructor(message?: string | undefined) { - super(message); + constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, InvalidStoreMethodError.prototype); } } @@ -159,8 +159,8 @@ export class InvalidStoreMethodError extends NonRetryableUserError { export class UndefinedTableError extends NonRetryableUserError { override name = "UndefinedTableError"; - constructor(message?: string | undefined) { - super(message); + constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, UndefinedTableError.prototype); } } @@ -168,8 +168,8 @@ export class UndefinedTableError extends NonRetryableUserError { export class BigIntSerializationError extends NonRetryableUserError { override name = "BigIntSerializationError"; - constructor(message?: string | undefined) { - super(message); + constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, BigIntSerializationError.prototype); } } @@ -177,8 +177,8 @@ export class BigIntSerializationError extends NonRetryableUserError { export class DelayedInsertError extends NonRetryableUserError { override name = "DelayedInsertError"; - constructor(message?: string | undefined) { - super(message); + constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, DelayedInsertError.prototype); } } @@ -186,8 +186,8 @@ export class DelayedInsertError extends NonRetryableUserError { export class RawSqlError extends NonRetryableUserError { override name = "RawSqlError"; - constructor(message?: string | undefined) { - super(message); + constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, RawSqlError.prototype); } } @@ -195,8 +195,8 @@ export class RawSqlError extends NonRetryableUserError { export class IndexingFunctionError extends NonRetryableUserError { override name = "IndexingFunctionError"; - constructor(message?: string | undefined) { - super(message); + constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, IndexingFunctionError.prototype); } } @@ -204,8 +204,8 @@ export class IndexingFunctionError extends NonRetryableUserError { export class RpcProviderError extends BaseError { override name = "RpcProviderError"; - constructor(message?: string | undefined) { - super(message); + constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, RpcProviderError.prototype); } } From 163fae5110a359ffa1a155663af2eb355082d6b8 Mon Sep 17 00:00:00 2001 From: Kyle Scott Date: Mon, 12 Jan 2026 14:02:07 -0500 Subject: [PATCH 02/11] more build error handling --- packages/core/src/bin/commands/createViews.ts | 23 +-- packages/core/src/bin/commands/dev.ts | 36 ++-- packages/core/src/bin/commands/list.ts | 17 +- packages/core/src/bin/commands/prune.ts | 17 +- packages/core/src/bin/commands/serve.ts | 27 +-- packages/core/src/bin/commands/start.ts | 27 ++- packages/core/src/build/config.test.ts | 165 ++++++++---------- packages/core/src/build/config.ts | 134 +++++--------- packages/core/src/build/index.ts | 134 +++++--------- packages/core/src/build/pre.ts | 26 +-- packages/core/src/build/schema.ts | 88 ++++------ 11 files changed, 270 insertions(+), 424 deletions(-) diff --git a/packages/core/src/bin/commands/createViews.ts b/packages/core/src/bin/commands/createViews.ts index 547d22af2..688a92eff 100644 --- a/packages/core/src/bin/commands/createViews.ts +++ b/packages/core/src/bin/commands/createViews.ts @@ -111,8 +111,20 @@ export async function createViews({ return; } + const database = createDatabase({ + common, + // Note: `namespace` is not used in this command + namespace: { + schema: cliOptions.schema!, + viewsSchema: undefined, + }, + preBuild: buildResult.result, + schemaBuild: emptySchemaBuild, + }); + const databaseDiagnostic = await build.databaseDiagnostic({ preBuild: buildResult.result, + database, }); if (databaseDiagnostic.status === "error") { common.logger.error({ @@ -124,17 +136,6 @@ export async function createViews({ return; } - const database = createDatabase({ - common, - // Note: `namespace` is not used in this command - namespace: { - schema: cliOptions.schema!, - viewsSchema: undefined, - }, - preBuild: buildResult.result, - schemaBuild: emptySchemaBuild, - }); - const endClock = startClock(); const schemaExists = await database.adminQB diff --git a/packages/core/src/bin/commands/dev.ts b/packages/core/src/bin/commands/dev.ts index 4f08388ac..5e33cc97b 100644 --- a/packages/core/src/bin/commands/dev.ts +++ b/packages/core/src/bin/commands/dev.ts @@ -162,23 +162,6 @@ export async function dev({ cliOptions }: { cliOptions: CliOptions }) { return; } - const databaseDiagnostic = await build.databaseDiagnostic({ - preBuild: preCompileResult.result, - }); - if (databaseDiagnostic.status === "error") { - common.logger.error({ - msg: "Build failed", - stage: "diagnostic", - error: databaseDiagnostic.error, - }); - buildQueue.add({ - status: "error", - kind: "indexing", - error: databaseDiagnostic.error, - }); - return; - } - const compileSchemaResult = build.compileSchema({ ...schemaResult.result, preBuild: preCompileResult.result, @@ -276,6 +259,25 @@ export async function dev({ cliOptions }: { cliOptions: CliOptions }) { preBuild: preCompileResult.result, schemaBuild: compileSchemaResult.result, }); + + const databaseDiagnostic = await build.databaseDiagnostic({ + preBuild: preCompileResult.result, + database, + }); + if (databaseDiagnostic.status === "error") { + common.logger.error({ + msg: "Build failed", + stage: "diagnostic", + error: databaseDiagnostic.error, + }); + buildQueue.add({ + status: "error", + kind: "indexing", + error: databaseDiagnostic.error, + }); + return; + } + crashRecoveryCheckpoint = await database.migrate({ buildId: indexingBuildResult.result.buildId, chains: indexingBuildResult.result.chains, diff --git a/packages/core/src/bin/commands/list.ts b/packages/core/src/bin/commands/list.ts index ba77fd74e..148a6a09b 100644 --- a/packages/core/src/bin/commands/list.ts +++ b/packages/core/src/bin/commands/list.ts @@ -84,8 +84,17 @@ export async function list({ cliOptions }: { cliOptions: CliOptions }) { return; } + const database = createDatabase({ + common, + // Note: `namespace` is not used in this command + namespace: { schema: "public", viewsSchema: undefined }, + preBuild: buildResult.result, + schemaBuild: emptySchemaBuild, + }); + const databaseDiagnostic = await build.databaseDiagnostic({ preBuild: buildResult.result, + database, }); if (databaseDiagnostic.status === "error") { common.logger.error({ @@ -97,14 +106,6 @@ export async function list({ cliOptions }: { cliOptions: CliOptions }) { return; } - const database = createDatabase({ - common, - // Note: `namespace` is not used in this command - namespace: { schema: "public", viewsSchema: undefined }, - preBuild: buildResult.result, - schemaBuild: emptySchemaBuild, - }); - const ponderSchemas = await database.adminQB.wrap((db) => db .select({ schema: TABLES.table_schema }) diff --git a/packages/core/src/bin/commands/prune.ts b/packages/core/src/bin/commands/prune.ts index f1b35d4b9..e3b5f2762 100644 --- a/packages/core/src/bin/commands/prune.ts +++ b/packages/core/src/bin/commands/prune.ts @@ -91,8 +91,17 @@ export async function prune({ cliOptions }: { cliOptions: CliOptions }) { return; } + const database = createDatabase({ + common, + // Note: `namespace` is not used in this command + namespace: { schema: "public", viewsSchema: undefined }, + preBuild: buildResult.result, + schemaBuild: emptySchemaBuild, + }); + const databaseDiagnostic = await build.databaseDiagnostic({ preBuild: buildResult.result, + database, }); if (databaseDiagnostic.status === "error") { common.logger.error({ @@ -104,14 +113,6 @@ export async function prune({ cliOptions }: { cliOptions: CliOptions }) { return; } - const database = createDatabase({ - common, - // Note: `namespace` is not used in this command - namespace: { schema: "public", viewsSchema: undefined }, - preBuild: buildResult.result, - schemaBuild: emptySchemaBuild, - }); - const ponderSchemas = await database.adminQB.wrap((db) => db .select({ schema: TABLES.table_schema, tableCount: count() }) diff --git a/packages/core/src/bin/commands/serve.ts b/packages/core/src/bin/commands/serve.ts index 53e2328f0..4130de26e 100644 --- a/packages/core/src/bin/commands/serve.ts +++ b/packages/core/src/bin/commands/serve.ts @@ -113,19 +113,6 @@ export async function serve({ cliOptions }: { cliOptions: CliOptions }) { return; } - const databaseDiagnostic = await build.databaseDiagnostic({ - preBuild: preCompileResult.result, - }); - if (databaseDiagnostic.status === "error") { - common.logger.error({ - msg: "Build failed", - stage: "diagnostic", - error: databaseDiagnostic.error, - }); - await exit({ code: 75 }); - return; - } - const compileSchemaResult = build.compileSchema({ ...schemaResult.result, preBuild: preCompileResult.result, @@ -164,6 +151,20 @@ export async function serve({ cliOptions }: { cliOptions: CliOptions }) { schemaBuild: compileSchemaResult.result, }); + const databaseDiagnostic = await build.databaseDiagnostic({ + preBuild: preCompileResult.result, + database, + }); + if (databaseDiagnostic.status === "error") { + common.logger.error({ + msg: "Build failed", + stage: "diagnostic", + error: databaseDiagnostic.error, + }); + await exit({ code: 75 }); + return; + } + const schemaExists = await database.adminQB .wrap((db) => db diff --git a/packages/core/src/bin/commands/start.ts b/packages/core/src/bin/commands/start.ts index c686acffe..95a69ab19 100644 --- a/packages/core/src/bin/commands/start.ts +++ b/packages/core/src/bin/commands/start.ts @@ -137,19 +137,6 @@ export async function start({ return; } - const databaseDiagnostic = await build.databaseDiagnostic({ - preBuild: preCompileResult.result, - }); - if (databaseDiagnostic.status === "error") { - common.logger.error({ - msg: "Build failed", - stage: "diagnostic", - error: databaseDiagnostic.error, - }); - await exit({ code: 75 }); - return; - } - const compileSchemaResult = build.compileSchema({ ...schemaResult.result, preBuild: preCompileResult.result, @@ -226,7 +213,19 @@ export async function start({ schemaBuild: compileSchemaResult.result, }); - // TODO(kyle) database diagnostic + const databaseDiagnostic = await build.databaseDiagnostic({ + preBuild: preCompileResult.result, + database, + }); + if (databaseDiagnostic.status === "error") { + common.logger.error({ + msg: "Build failed", + stage: "diagnostic", + error: databaseDiagnostic.error, + }); + await exit({ code: 75 }); + return; + } const crashRecoveryCheckpoint = await database.migrate({ buildId: indexingBuildResult.result.buildId, diff --git a/packages/core/src/build/config.test.ts b/packages/core/src/build/config.test.ts index 83a884e45..735f9fdc1 100644 --- a/packages/core/src/build/config.test.ts +++ b/packages/core/src/build/config.test.ts @@ -12,12 +12,7 @@ import { zeroAddress, } from "viem"; import { beforeEach, expect, test } from "vitest"; -import { - buildConfig, - buildIndexingFunctions, - safeBuildConfig, - safeBuildIndexingFunctions, -} from "./config.js"; +import { buildConfig, buildIndexingFunctions } from "./config.js"; beforeEach(setupCommon); beforeEach(setupAnvil); @@ -209,16 +204,15 @@ test("buildIndexingFunctions() throw useful error for common 0.11 migration mist config, }); - const result = await safeBuildIndexingFunctions({ - common: context.common, - // @ts-expect-error - config, - indexingFunctions, - configBuild, - }); - - expect(result.status).toBe("error"); - expect(result.error?.message).toBe( + await expect( + buildIndexingFunctions({ + common: context.common, + // @ts-expect-error + config, + indexingFunctions, + configBuild, + }), + ).rejects.toThrowError( "Validation failed: Chain for 'a' is null or undefined. Expected one of ['mainnet', 'optimism']. Did you forget to change 'network' to 'chain' when migrating to 0.11?", ); }); @@ -402,15 +396,14 @@ test("buildIndexingFunctions() validates chain name", async () => { common: context.common, config, }); - const result = await safeBuildIndexingFunctions({ - common: context.common, - config, - indexingFunctions: [{ name: "a:Event0", fn: () => {} }], - configBuild, - }); - - expect(result.status).toBe("error"); - expect(result.error?.message).toBe( + await expect( + buildIndexingFunctions({ + common: context.common, + config, + indexingFunctions: [{ name: "a:Event0", fn: () => {} }], + configBuild, + }), + ).rejects.toThrowError( "Validation failed: Invalid chain for 'a'. Got 'mainnetz', expected one of ['mainnet'].", ); }); @@ -430,13 +423,12 @@ test.skip("buildConfig() warns for public RPC URL", () => { }, }); - const result = safeBuildConfig({ + const { logs } = buildConfig({ common: context.common, config, }); - expect(result.status).toBe("success"); - expect(result.logs!.filter((l) => l.level === "warn")).toEqual([ + expect(logs!.filter((l) => l.level === "warn")).toEqual([ { level: "warn", msg: "Chain 'mainnet' is using a public RPC URL (https://cloudflare-eth.com). Most apps require an RPC URL with a higher rate limit.", @@ -458,12 +450,7 @@ test("buildConfig() handles chains not found in viem", () => { }, }); - const result = safeBuildConfig({ - common: context.common, - config, - }); - - expect(result.status).toBe("success"); + buildConfig({ common: context.common, config }); }); test("buildIndexingFunctions() validates event filter event name must be present in ABI", async () => { @@ -490,15 +477,14 @@ test("buildIndexingFunctions() validates event filter event name must be present common: context.common, config, }); - const result = await safeBuildIndexingFunctions({ - common: context.common, - config, - indexingFunctions: [{ name: "a:Event0", fn: () => {} }], - configBuild, - }); - - expect(result.status).toBe("error"); - expect(result.error?.message).toBe( + await expect( + buildIndexingFunctions({ + common: context.common, + config, + indexingFunctions: [{ name: "a:Event0", fn: () => {} }], + configBuild, + }), + ).rejects.toThrowError( "Validation failed: Invalid filter for contract 'a'. Got event name 'Event2', expected one of ['Event0'].", ); }); @@ -522,15 +508,14 @@ test("buildIndexingFunctions() validates address empty string", async () => { config, }); - const result = await safeBuildIndexingFunctions({ - common: context.common, - config, - indexingFunctions: [{ name: "a:Event0", fn: () => {} }], - configBuild, - }); - - expect(result.status).toBe("error"); - expect(result.error?.message).toBe( + await expect( + buildIndexingFunctions({ + common: context.common, + config, + indexingFunctions: [{ name: "a:Event0", fn: () => {} }], + configBuild, + }), + ).rejects.toThrowError( "Validation failed: Invalid prefix for address ''. Got '', expected '0x'.", ); }); @@ -551,15 +536,14 @@ test("buildIndexingFunctions() validates address prefix", async () => { }); const configBuild = buildConfig({ common: context.common, config }); - const result = await safeBuildIndexingFunctions({ - common: context.common, - config, - indexingFunctions: [{ name: "a:Event0", fn: () => {} }], - configBuild, - }); - - expect(result.status).toBe("error"); - expect(result.error?.message).toBe( + await expect( + buildIndexingFunctions({ + common: context.common, + config, + indexingFunctions: [{ name: "a:Event0", fn: () => {} }], + configBuild, + }), + ).rejects.toThrowError( "Validation failed: Invalid prefix for address '0b0000000000000000000000000000000000000001'. Got '0b', expected '0x'.", ); }); @@ -579,15 +563,14 @@ test("buildIndexingFunctions() validates address length", async () => { }); const configBuild = buildConfig({ common: context.common, config }); - const result = await safeBuildIndexingFunctions({ - common: context.common, - config, - indexingFunctions: [{ name: "a:Event0", fn: () => {} }], - configBuild, - }); - - expect(result.status).toBe("error"); - expect(result.error?.message).toBe( + await expect( + buildIndexingFunctions({ + common: context.common, + config, + indexingFunctions: [{ name: "a:Event0", fn: () => {} }], + configBuild, + }), + ).rejects.toThrowError( "Validation failed: Invalid length for address '0x000000000001'. Got 14, expected 42 characters.", ); }); @@ -923,15 +906,14 @@ test("buildIndexingFunctions() validates factory interval", async () => { }); const configBuild = buildConfig({ common: context.common, config }); - const result = await safeBuildIndexingFunctions({ - common: context.common, - config, - indexingFunctions: [{ name: "a:Event0", fn: () => {} }], - configBuild, - }); - - expect(result.status).toBe("error"); - expect(result.error?.message).toBe( + await expect( + buildIndexingFunctions({ + common: context.common, + config, + indexingFunctions: [{ name: "a:Event0", fn: () => {} }], + configBuild, + }), + ).rejects.toThrowError( "Validation failed: Start block for 'a' is before start block of factory address (16370050 > 16370000).", ); }); @@ -955,20 +937,17 @@ test("buildIndexingFunctions() validates start and end block", async () => { // @ts-expect-error const configBuild = buildConfig({ common: context.common, config }); - const result = await safeBuildIndexingFunctions({ - common: context.common, - // @ts-expect-error - config, - indexingFunctions: [{ name: "a:Event0", fn: () => {} }], - configBuild, - }); - - expect(result).toMatchInlineSnapshot(` - { - "error": [BuildError: Validation failed: Invalid start block for 'a'. Got 16370000 typeof string, expected an integer.], - "status": "error", - } - `); + await expect( + buildIndexingFunctions({ + common: context.common, + // @ts-expect-error + config, + indexingFunctions: [{ name: "a:Event0", fn: () => {} }], + configBuild, + }), + ).rejects.toThrowError( + "Validation failed: Invalid start block for 'a'. Got 16370000 typeof string, expected an integer.", + ); }); test("buildIndexingFunctions() returns chain, rpc, and finalized block", async () => { diff --git a/packages/core/src/build/config.ts b/packages/core/src/build/config.ts index f640198d5..5a2f930f3 100644 --- a/packages/core/src/build/config.ts +++ b/packages/core/src/build/config.ts @@ -144,9 +144,10 @@ export async function buildIndexingFunctions({ throw new BlockNotFoundError({ blockNumber: "latest" as any }); return hexToNumber((block as SyncBlock).number); }) - .catch((e) => { - throw new Error( - `Unable to fetch "latest" block for chain '${chain.name}':\n${e.message}`, + .catch((_error) => { + throw new BuildError( + `Unable to fetch "latest" block for chain '${chain.name}'`, + { cause: _error }, ); }); perChainLatestBlockNumber.set(chain.name, blockPromise); @@ -165,9 +166,10 @@ export async function buildIndexingFunctions({ retryNullBlockRequest: true, }) .then((block) => hexToNumber((block as SyncBlock).number)) - .catch((e) => { - throw new Error( - `Unable to fetch "latest" block for chain '${chain.name}':\n${e.message}`, + .catch((_error) => { + throw new BuildError( + `Unable to fetch "latest" block for chain '${chain.name}'`, + { cause: _error }, ); }); @@ -195,7 +197,7 @@ export async function buildIndexingFunctions({ ...Object.keys(config.blocks ?? {}), ]) { if (sourceNames.has(source)) { - throw new Error( + throw new BuildError( `Validation failed: Duplicate name '${source}' not allowed. The name must be unique across blocks, contracts, and accounts.`, ); } @@ -204,7 +206,7 @@ export async function buildIndexingFunctions({ // Validate and build indexing functions if (indexingFunctions.length === 0) { - throw new Error( + throw new BuildError( "Validation failed: Found 0 registered indexing functions.", ); } @@ -219,7 +221,7 @@ export async function buildIndexingFunctions({ const [sourceName] = eventNameComponents; if (!sourceName) { - throw new Error( + throw new BuildError( `Validation failed: Invalid event '${eventName}', expected format '{sourceName}:{eventName}' or '{sourceName}.{functionName}'.`, ); } @@ -231,7 +233,7 @@ export async function buildIndexingFunctions({ (sourceType !== "transaction" && sourceType !== "transfer") || (fromOrTo !== "from" && fromOrTo !== "to") ) { - throw new Error( + throw new BuildError( `Validation failed: Invalid event '${eventName}', expected format '{sourceName}:transaction:from', '{sourceName}:transaction:to', '{sourceName}:transfer:from', or '{sourceName}:transfer:to'.`, ); } @@ -239,18 +241,18 @@ export async function buildIndexingFunctions({ const [, sourceEventName] = eventNameComponents; if (!sourceEventName) { - throw new Error( + throw new BuildError( `Validation failed: Invalid event '${eventName}', expected format '{sourceName}:{eventName}' or '{sourceName}.{functionName}'.`, ); } } else { - throw new Error( + throw new BuildError( `Validation failed: Invalid event '${eventName}', expected format '{sourceName}:{eventName}' or '{sourceName}.{functionName}'.`, ); } if (eventNames.has(eventName)) { - throw new Error( + throw new BuildError( `Validation failed: Multiple indexing functions registered for event '${eventName}'.`, ); } @@ -265,7 +267,7 @@ export async function buildIndexingFunctions({ }).find((_sourceName) => _sourceName === sourceName); if (!matchedSourceName) { - throw new Error( + throw new BuildError( `Validation failed: Invalid event '${eventName}' uses an unrecognized contract, account, or block interval name. Expected one of [${Array.from( sourceNames, ) @@ -282,7 +284,7 @@ export async function buildIndexingFunctions({ ...flattenSources(config.blocks ?? {}), ]) { if (source.chain === null || source.chain === undefined) { - throw new Error( + throw new BuildError( `Validation failed: Chain for '${source.name}' is null or undefined. Expected one of [${chains .map((n) => `'${n.name}'`) .join( @@ -293,7 +295,7 @@ export async function buildIndexingFunctions({ const chain = chains.find((n) => n.name === source.chain); if (!chain) { - throw new Error( + throw new BuildError( `Validation failed: Invalid chain for '${ source.name }'. Got '${source.chain}', expected one of [${chains @@ -310,19 +312,19 @@ export async function buildIndexingFunctions({ endBlock !== undefined && endBlock < startBlock ) { - throw new Error( + throw new BuildError( `Validation failed: Start block for '${source.name}' is after end block (${startBlock} > ${endBlock}).`, ); } if (startBlock !== undefined && Number.isInteger(startBlock) === false) { - throw new Error( + throw new BuildError( `Validation failed: Invalid start block for '${source.name}'. Got ${startBlock} typeof ${typeof startBlock}, expected an integer.`, ); } if (endBlock !== undefined && Number.isInteger(endBlock) === false) { - throw new Error( + throw new BuildError( `Validation failed: Invalid end block for '${source.name}'. Got ${endBlock} typeof ${typeof endBlock}, expected an integer.`, ); } @@ -343,7 +345,7 @@ export async function buildIndexingFunctions({ factoryStartBlock !== undefined && (startBlock === undefined || factoryStartBlock > startBlock) ) { - throw new Error( + throw new BuildError( `Validation failed: Start block for '${source.name}' is before start block of factory address (${factoryStartBlock} > ${startBlock}).`, ); } @@ -352,7 +354,7 @@ export async function buildIndexingFunctions({ endBlock !== undefined && (factoryEndBlock === undefined || factoryEndBlock > endBlock) ) { - throw new Error( + throw new BuildError( `Validation failed: End block for ${source.name} is before end block of factory address (${factoryEndBlock} > ${endBlock}).`, ); } @@ -362,7 +364,7 @@ export async function buildIndexingFunctions({ factoryEndBlock !== undefined && factoryEndBlock < factoryStartBlock ) { - throw new Error( + throw new BuildError( `Validation failed: Start block for '${source.name}' factory address is after end block (${factoryStartBlock} > ${factoryEndBlock}).`, ); } @@ -437,14 +439,14 @@ export async function buildIndexingFunctions({ ? resolvedAddress : [resolvedAddress as Address]) { if (!address!.startsWith("0x")) - throw new Error( + throw new BuildError( `Validation failed: Invalid prefix for address '${address}'. Got '${address!.slice( 0, 2, )}', expected '0x'.`, ); if (address!.length !== 42) - throw new Error( + throw new BuildError( `Validation failed: Invalid length for address '${address}'. Got ${address!.length}, expected 42 characters.`, ); } @@ -483,7 +485,7 @@ export async function buildIndexingFunctions({ toSafeName({ abi: source.abi, item }) === filter.event, ); if (!abiEvent) { - throw new Error( + throw new BuildError( `Validation failed: Invalid filter for contract '${ source.name }'. Got event name '${filter.event}', expected one of [${source.abi @@ -505,7 +507,7 @@ export async function buildIndexingFunctions({ ); if (indexingFunction === undefined) { - throw new Error( + throw new BuildError( `Validation failed: Event selector '${toSafeName({ abi: source.abi, item: abiItem })}' is used in a filter but does not have a corresponding indexing function.`, ); } @@ -551,7 +553,7 @@ export async function buildIndexingFunctions({ toSafeName({ abi: source.abi, item }) === logEventName, ); if (abiEvent === undefined) { - throw new Error( + throw new BuildError( `Validation failed: Event name for event '${logEventName}' not found in the contract ABI. Got '${logEventName}', expected one of [${source.abi .filter((item): item is AbiEvent => item.type === "event") .map((item) => `'${toSafeName({ abi: source.abi, item })}'`) @@ -625,7 +627,7 @@ export async function buildIndexingFunctions({ toSafeName({ abi: source.abi, item }) === functionEventName, ); if (abiFunction === undefined) { - throw new Error( + throw new BuildError( `Validation failed: Function name for function '${functionEventName}' not found in the contract ABI. Got '${functionEventName}', expected one of [${source.abi .filter((item): item is AbiFunction => item.type === "function") .map((item) => `'${toSafeName({ abi: source.abi, item })}'`) @@ -697,7 +699,7 @@ export async function buildIndexingFunctions({ const resolvedAddress = source?.address; if (resolvedAddress === undefined) { - throw new Error( + throw new BuildError( `Validation failed: Account '${source.name}' must specify an 'address'.`, ); } @@ -730,14 +732,14 @@ export async function buildIndexingFunctions({ ? resolvedAddress : [resolvedAddress]) { if (!address!.startsWith("0x")) - throw new Error( + throw new BuildError( `Validation failed: Invalid prefix for address '${address}'. Got '${address!.slice( 0, 2, )}', expected '0x'.`, ); if (address!.length !== 42) - throw new Error( + throw new BuildError( `Validation failed: Invalid length for address '${address}'. Got ${address!.length}, expected 42 characters.`, ); } @@ -868,7 +870,7 @@ export async function buildIndexingFunctions({ const interval = Number.isNaN(intervalMaybeNan) ? 0 : intervalMaybeNan; if (!Number.isInteger(interval) || interval === 0) { - throw new Error( + throw new BuildError( `Validation failed: Invalid interval for block interval '${source.name}'. Got ${interval}, expected a non-zero integer.`, ); } @@ -947,7 +949,7 @@ export async function buildIndexingFunctions({ } if (chainsWithSources.length === 0) { - throw new Error( + throw new BuildError( "Validation failed: Found 0 chains with registered indexing functions.", ); } @@ -982,7 +984,7 @@ export function buildConfig({ const chains: Chain[] = Object.entries(config.chains).map( ([chainName, chain]) => { if (chain.id > Number.MAX_SAFE_INTEGER) { - throw new Error( + throw new BuildError( `Chain "${chainName}" with id ${chain.id} has a chain_id that is too large.`, ); } @@ -996,7 +998,7 @@ export function buildConfig({ if (chain.rpc === undefined || chain.rpc === "") { if (matchedChain === undefined) { - throw new Error( + throw new BuildError( `Chain "${chainName}" with id ${chain.id} has no RPC defined and no default RPC URL was found in 'viem/chains'.`, ); } @@ -1008,7 +1010,7 @@ export function buildConfig({ const rpcs = Array.isArray(chain.rpc) ? chain.rpc : [chain.rpc]; if (rpcs.length === 0) { - throw new Error( + throw new BuildError( `Chain "${chainName}" with id ${chain.id} has no RPC URLs.`, ); } @@ -1044,7 +1046,7 @@ export function buildConfig({ } if (chain.pollingInterval !== undefined && chain.pollingInterval! < 100) { - throw new Error( + throw new BuildError( `Invalid 'pollingInterval' for chain '${chainName}. Expected 100 milliseconds or greater, got ${chain.pollingInterval} milliseconds.`, ); } @@ -1066,7 +1068,7 @@ export function buildConfig({ const chainIds = new Set(); for (const chain of chains) { if (chainIds.has(chain.id)) { - throw new Error( + throw new BuildError( `Invalid id for chain "${chain.name}". ${chain.id} is already in use.`, ); } @@ -1083,59 +1085,3 @@ export function buildConfig({ return { chains, rpcs, logs }; } - -export async function safeBuildIndexingFunctions({ - common, - config, - indexingFunctions, - configBuild, -}: { - common: Common; - config: Config; - indexingFunctions: IndexingFunctions; - configBuild: Pick; -}) { - try { - const result = await buildIndexingFunctions({ - common, - config, - indexingFunctions, - configBuild, - }); - - return { - status: "success", - chains: result.chains, - rpcs: result.rpcs, - finalizedBlocks: result.finalizedBlocks, - eventCallbacks: result.eventCallbacks, - setupCallbacks: result.setupCallbacks, - contracts: result.contracts, - logs: result.logs, - } as const; - } catch (_error) { - const buildError = new BuildError((_error as Error).message); - buildError.stack = undefined; - return { status: "error", error: buildError } as const; - } -} - -export function safeBuildConfig({ - common, - config, -}: { common: Common; config: Config }) { - try { - const result = buildConfig({ common, config }); - - return { - status: "success", - chains: result.chains, - rpcs: result.rpcs, - logs: result.logs, - } as const; - } catch (_error) { - const buildError = new BuildError((_error as Error).message); - buildError.stack = undefined; - return { status: "error", error: buildError } as const; - } -} diff --git a/packages/core/src/build/index.ts b/packages/core/src/build/index.ts index cd77546b1..4996bfd0d 100644 --- a/packages/core/src/build/index.ts +++ b/packages/core/src/build/index.ts @@ -4,14 +4,9 @@ import path from "node:path"; import type { CliOptions } from "@/bin/ponder.js"; import type { Config } from "@/config/index.js"; import type { Database } from "@/database/index.js"; -import { createQB } from "@/database/queryBuilder.js"; import { MAX_DATABASE_OBJECT_NAME_LENGTH } from "@/drizzle/onchain.js"; import type { Common } from "@/internal/common.js"; -import { - BuildError, - NonRetryableUserError, - RetryableError, -} from "@/internal/errors.js"; +import { BuildError } from "@/internal/errors.js"; import type { ApiBuild, IndexingBuild, @@ -21,13 +16,10 @@ import type { Schema, SchemaBuild, } from "@/internal/types.js"; -import { createPool, getDatabaseName } from "@/utils/pg.js"; -import { createPglite } from "@/utils/pglite.js"; +import { getDatabaseName } from "@/utils/pg.js"; import { getNextAvailablePort } from "@/utils/port.js"; import type { Result } from "@/utils/result.js"; import { startClock } from "@/utils/timer.js"; -import { drizzle as drizzleNodePostgres } from "drizzle-orm/node-postgres"; -import { drizzle as drizzlePglite } from "drizzle-orm/pglite"; import { glob } from "glob"; import { Hono } from "hono"; import superjson from "superjson"; @@ -38,10 +30,10 @@ import { ViteNodeServer } from "vite-node/server"; import { installSourcemapsSupport } from "vite-node/source-map"; import { normalizeModuleId, toFilePath } from "vite-node/utils"; import viteTsconfigPathsPlugin from "vite-tsconfig-paths"; -import { safeBuildConfig, safeBuildIndexingFunctions } from "./config.js"; +import { buildConfig, buildIndexingFunctions } from "./config.js"; import { vitePluginPonder } from "./plugin.js"; -import { safeBuildPre } from "./pre.js"; -import { safeBuildSchema } from "./schema.js"; +import { buildPre } from "./pre.js"; +import { buildSchema } from "./schema.js"; import { parseViteNodeError } from "./stacktrace.js"; declare global { @@ -93,7 +85,10 @@ export type Build = { rpcDiagnostic: (params: { configBuild: Pick; }) => Promise>; - databaseDiagnostic: (params: { preBuild: PreBuild }) => Promise>; + databaseDiagnostic: (params: { + preBuild: Pick; + database: Database; + }) => Promise>; }; export const createBuild = async ({ @@ -189,18 +184,19 @@ export const createBuild = async ({ } }; + // TODO(kyle) all files should use this function const executeFileWithTimeout = async ({ file, }: { file: string }): Promise< { status: "success"; exports: any } | { status: "error"; error: Error } > => { let timeoutId: ReturnType; - const timeout = new Promise((resolve) => { + const timeout = new Promise((resolve) => { timeoutId = setTimeout( () => resolve( - new NonRetryableUserError( - "File execution did not complete (waited 10s)", + new BuildError( + `Executing file "${path.relative(common.options.rootDir, file)}" took longer than 10 seconds`, ), ), 10_000, @@ -208,7 +204,7 @@ export const createBuild = async ({ }); const result = await Promise.race([executeFile({ file }), timeout]); - if (result instanceof NonRetryableUserError) { + if (result instanceof BuildError) { return { status: "error", error: result }; } @@ -218,7 +214,7 @@ export const createBuild = async ({ const build = { async executeConfig(): Promise { - const executeResult = await executeFile({ + const executeResult = await executeFileWithTimeout({ file: common.options.configFile, }); @@ -245,7 +241,7 @@ export const createBuild = async ({ } as const; }, async executeSchema(): Promise { - const executeResult = await executeFile({ + const executeResult = await executeFileWithTimeout({ file: common.options.schemaFile, }); @@ -314,7 +310,9 @@ export const createBuild = async ({ return { status: "error", error }; } - const executeResult = await executeFile({ file: common.options.apiFile }); + const executeResult = await executeFileWithTimeout({ + file: common.options.apiFile, + }); if (executeResult.status === "error") { return executeResult; @@ -394,14 +392,11 @@ export const createBuild = async ({ } as const; }, preCompile({ config }): Result { - const preBuild = safeBuildPre({ + const preBuild = buildPre({ config, options: common.options, logger: common.logger, }); - if (preBuild.status === "error") { - return preBuild; - } return { status: "success", @@ -412,29 +407,18 @@ export const createBuild = async ({ } as const; }, compileSchema({ schema, preBuild }) { - const buildSchemaResult = safeBuildSchema({ schema, preBuild }); - - if (buildSchemaResult.status === "error") { - return buildSchemaResult; - } + const { statements } = buildSchema({ schema, preBuild }); return { status: "success", - result: { - schema, - statements: buildSchemaResult.statements, - }, + result: { schema, statements }, } as const; }, compileConfig({ configResult }) { - // Validates and builds the config - const buildConfigResult = safeBuildConfig({ + const buildConfigResult = buildConfig({ common, config: configResult.config, }); - if (buildConfigResult.status === "error") { - return buildConfigResult; - } for (const log of buildConfigResult.logs) { const { level, ...rest } = log; @@ -455,16 +439,12 @@ export const createBuild = async ({ indexingResult, configBuild, }) { - // Validates and builds the config - const buildIndexingFunctionsResult = await safeBuildIndexingFunctions({ + const buildIndexingFunctionsResult = await buildIndexingFunctions({ common, config: configResult.config, indexingFunctions: indexingResult.indexingFunctions, configBuild, }); - if (buildIndexingFunctionsResult.status === "error") { - return buildIndexingFunctionsResult; - } for (const log of buildIndexingFunctionsResult.logs) { const { level, ...rest } = log; @@ -681,65 +661,43 @@ export const createBuild = async ({ return { status: "success", result: undefined }; }, - async databaseDiagnostic({ preBuild }) { + async databaseDiagnostic({ preBuild, database }) { const context = { logger: common.logger.child({ action: "database_diagnostic" }), }; const endClock = startClock(); - const dialect = preBuild.databaseConfig.kind; - if (dialect === "pglite") { - const driver = createPglite(preBuild.databaseConfig.options); - const qb = createQB(drizzlePglite(driver), { common }); - try { - await qb.wrap((db) => db.execute("SELECT version()"), context); - } catch (e) { - const error = new RetryableError( - `Failed to connect to PGlite database. Please check your database connection settings.\n\n${(e as any).message}`, - ); - return { status: "error", error }; - } finally { - await driver.close(); - } + try { + await database.adminQB.wrap( + (db) => db.execute("SELECT version()"), + context, + ); + } catch (_error) { + const error = new BuildError("Failed to connect to database", { + cause: _error as Error, + }); + return { status: "error", error } as const; + } + if (preBuild.databaseConfig.kind === "postgres") { + common.logger.info({ + msg: "Connected to database", + type: database.driver.dialect, + database: getDatabaseName(preBuild.databaseConfig.poolConfig), + duration: endClock(), + }); + } else if (preBuild.databaseConfig.kind === "pglite") { const pgliteDir = preBuild.databaseConfig.options.dataDir; const pglitePath = pgliteDir === "memory://" ? "memory://" : path.relative(common.options.rootDir, pgliteDir); - common.logger.info({ - msg: "Connected to database", - type: dialect, - database: pglitePath, - duration: endClock(), - }); - } else if (dialect === "postgres") { - const pool = createPool( - { - ...preBuild.databaseConfig.poolConfig, - application_name: "test", - max: 1, - statement_timeout: 10_000, - }, - common.logger, - ); - const qb = createQB(drizzleNodePostgres(pool), { common }); - try { - await qb.wrap((db) => db.execute("SELECT version()"), context); - } catch (e) { - const error = new RetryableError( - `Failed to connect to database. Please check your database connection settings.\n\n${(e as any).message}`, - ); - return { status: "error", error }; - } finally { - await pool.end(); - } common.logger.info({ msg: "Connected to database", - type: dialect, - database: getDatabaseName(preBuild.databaseConfig.poolConfig), + type: database.driver.dialect, + database: pglitePath, duration: endClock(), }); } diff --git a/packages/core/src/build/pre.ts b/packages/core/src/build/pre.ts index 1afc56f80..59310985b 100644 --- a/packages/core/src/build/pre.ts +++ b/packages/core/src/build/pre.ts @@ -42,7 +42,7 @@ export function buildPre({ if (connectionString === undefined) { if (config.database.poolConfig === undefined) { - throw new Error( + throw new BuildError( "Invalid database configuration: Either 'connectionString' or 'poolConfig' must be defined.", ); } @@ -88,27 +88,3 @@ export function buildPre({ ordering: config.ordering ?? "multichain", }; } - -export function safeBuildPre({ - config, - options, - logger, -}: { - config: Config; - options: Pick; - logger: Logger; -}) { - try { - const result = buildPre({ config, options, logger }); - - return { - status: "success", - databaseConfig: result.databaseConfig, - ordering: result.ordering, - } as const; - } catch (_error) { - const buildError = new BuildError((_error as Error).message); - buildError.stack = undefined; - return { status: "error", error: buildError } as const; - } -} diff --git a/packages/core/src/build/schema.ts b/packages/core/src/build/schema.ts index a3c2c740a..34b255b52 100644 --- a/packages/core/src/build/schema.ts +++ b/packages/core/src/build/schema.ts @@ -51,13 +51,13 @@ export const buildSchema = ({ name === PONDER_META_TABLE_NAME || name === PONDER_CHECKPOINT_TABLE_NAME ) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}' is a reserved table name.`, ); } if (name.length > MAX_DATABASE_OBJECT_NAME_LENGTH) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}' table name cannot be longer than ${MAX_DATABASE_OBJECT_NAME_LENGTH} characters.`, ); } @@ -67,7 +67,7 @@ export const buildSchema = ({ for (const [columnName, column] of Object.entries(getTableColumns(s))) { if (column.primary) { if (hasPrimaryKey) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}' has multiple primary keys.`, ); } else { @@ -81,44 +81,44 @@ export const buildSchema = ({ column instanceof PgBigSerial53 || column instanceof PgBigSerial64 ) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}.${columnName}' has a serial column and serial columns are unsupported.`, ); } if (column.isUnique) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}.${columnName}' has a unique constraint and unique constraints are unsupported.`, ); } if (column.generated !== undefined) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}.${columnName}' is a generated column and generated columns are unsupported.`, ); } if (column.generatedIdentity !== undefined) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}.${columnName}' is a generated column and generated columns are unsupported.`, ); } if (column.hasDefault) { if (column.default && column.default instanceof SQL) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}.${columnName}' is a default column and default columns with raw sql are unsupported.`, ); } if (column.defaultFn && column.defaultFn() instanceof SQL) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}.${columnName}' is a default column and default columns with raw sql are unsupported.`, ); } if (column.onUpdateFn && column.onUpdateFn() instanceof SQL) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}.${columnName}' is a default column and default columns with raw sql are unsupported.`, ); } @@ -136,13 +136,13 @@ export const buildSchema = ({ column.name === "operation" || column.name === "checkpoint" ) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}.${columnName}' is a reserved column name.`, ); } if (columnNames.has(column.name)) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}.${column.name}' column name is used multiple times.`, ); } else { @@ -152,7 +152,7 @@ export const buildSchema = ({ if (preBuild.ordering === "experimental_isolated") { if (hasChainIdColumn === false) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}' does not have required 'chainId' column.`, ); } @@ -161,7 +161,7 @@ export const buildSchema = ({ getTableColumns(s).chainId!.dataType !== "number" && getTableColumns(s).chainId!.dataType !== "bigint" ) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}'.chainId column must be an integer or numeric.`, ); } @@ -170,14 +170,14 @@ export const buildSchema = ({ getPrimaryKeyColumns(s).some(({ sql }) => sql === "chain_id") === false ) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}.chain_id' column is required to be in the primary key when ordering is 'isolated'.`, ); } } if (tableNames.has(getTableName(s))) { - throw new Error( + throw new BuildError( `Schema validation failed: table name '${getTableName(s)}' is used multiple times.`, ); } else { @@ -185,13 +185,13 @@ export const buildSchema = ({ } if (getTableConfig(s).primaryKeys.length > 1) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}' has multiple primary keys.`, ); } if (getTableConfig(s).primaryKeys.length === 1 && hasPrimaryKey) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}' has multiple primary keys.`, ); } @@ -200,25 +200,25 @@ export const buildSchema = ({ getTableConfig(s).primaryKeys.length === 0 && hasPrimaryKey === false ) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}' has no primary key. Declare one with ".primaryKey()".`, ); } if (getTableConfig(s).foreignKeys.length > 0) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}' has a foreign key constraint and foreign key constraints are unsupported.`, ); } if (getTableConfig(s).checks.length > 0) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}' has a check constraint and check constraints are unsupported.`, ); } if (getTableConfig(s).uniqueConstraints.length > 0) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}' has a unique constraint and unique constraints are unsupported.`, ); } @@ -227,7 +227,7 @@ export const buildSchema = ({ // Note: Ponder lets postgres handle the index name length limit and truncation. if (index.config.name && indexNames.has(index.config.name)) { - throw new Error( + throw new BuildError( `Schema validation failed: index name '${index.config.name}' is used multiple times.`, ); } else if (index.config.name) { @@ -237,7 +237,7 @@ export const buildSchema = ({ } if (is(s, PgSequence)) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}' is a sequence and sequences are unsupported.`, ); } @@ -246,7 +246,7 @@ export const buildSchema = ({ // Note: Ponder lets postgres handle the view name length limit and truncation. if (viewNames.has(getViewName(s))) { - throw new Error( + throw new BuildError( `Schema validation failed: view name '${getViewName(s)}' is used multiple times.`, ); } else { @@ -256,19 +256,19 @@ export const buildSchema = ({ const viewConfig = getViewConfig(s); if (viewConfig.selectedFields.length === 0) { - throw new Error( + throw new BuildError( `Schema validation failed: view '${getViewName(s)}' has no selected fields.`, ); } if (viewConfig.isExisting) { - throw new Error( + throw new BuildError( `Schema validation failed: view '${getViewName(s)}' is an existing view and existing views are unsupported.`, ); } if (viewConfig.query === undefined) { - throw new Error( + throw new BuildError( `Schema validation failed: view '${getViewName(s)}' has no underlying query.`, ); } @@ -283,7 +283,7 @@ export const buildSchema = ({ is(column, PgColumn) === false && is(column, SQL.Aliased) === false ) { - throw new Error( + throw new BuildError( `Schema validation failed: view '${getViewName(s)}.${columnName}' is a non-column selected field.`, ); } @@ -295,31 +295,31 @@ export const buildSchema = ({ column instanceof PgBigSerial53 || column instanceof PgBigSerial64 ) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}.${columnName}' has a serial column and serial columns are unsupported.`, ); } if (column.isUnique) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}.${columnName}' has a unique constraint and unique constraints are unsupported.`, ); } if (column.generated !== undefined) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}.${columnName}' is a generated column and generated columns are unsupported.`, ); } if (column.generatedIdentity !== undefined) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}.${columnName}' is a generated column and generated columns are unsupported.`, ); } if (columnNames.has((column as PgColumn).name)) { - throw new Error( + throw new BuildError( `Schema validation failed: '${name}.${(column as PgColumn).name}' column name is used multiple times.`, ); } else { @@ -331,28 +331,10 @@ export const buildSchema = ({ } if (tableNames.size > TABLE_LIMIT) { - throw new Error( + throw new BuildError( `Schema validation failed: the maximum number of tables is ${TABLE_LIMIT}.`, ); } return { statements }; }; - -export const safeBuildSchema = ({ - schema, - preBuild, -}: { schema: Schema; preBuild: Pick }) => { - try { - const result = buildSchema({ schema, preBuild }); - - return { - status: "success", - ...result, - } as const; - } catch (_error) { - const buildError = new BuildError((_error as Error).message); - buildError.stack = undefined; - return { status: "error", error: buildError } as const; - } -}; From ce5b7c1833105a3b14ca925bd976b99a19a118df Mon Sep 17 00:00:00 2001 From: Kyle Scott Date: Mon, 12 Jan 2026 14:04:26 -0500 Subject: [PATCH 03/11] remove error stack from rpc validations --- packages/core/src/rpc/actions.ts | 53 -------------------------------- 1 file changed, 53 deletions(-) diff --git a/packages/core/src/rpc/actions.ts b/packages/core/src/rpc/actions.ts index 32ba7a8ea..0d0e4dc52 100644 --- a/packages/core/src/rpc/actions.ts +++ b/packages/core/src/rpc/actions.ts @@ -327,7 +327,6 @@ export const validateTransactionsAndBlock = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } @@ -339,7 +338,6 @@ export const validateTransactionsAndBlock = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } } @@ -369,7 +367,6 @@ export const validateLogsAndBlock = ( requestText(blockRequest), requestText(logsRequest), ]; - error.stack = undefined; throw error; } @@ -390,7 +387,6 @@ export const validateLogsAndBlock = ( requestText(blockRequest), requestText(logsRequest), ]; - error.stack = undefined; throw error; } @@ -403,7 +399,6 @@ export const validateLogsAndBlock = ( requestText(blockRequest), requestText(logsRequest), ]; - error.stack = undefined; throw error; } @@ -418,7 +413,6 @@ export const validateLogsAndBlock = ( requestText(blockRequest), requestText(logsRequest), ]; - error.stack = undefined; throw error; } else if (transaction.hash !== log.transactionHash) { const error = new RpcProviderError( @@ -429,7 +423,6 @@ export const validateLogsAndBlock = ( requestText(blockRequest), requestText(logsRequest), ]; - error.stack = undefined; throw error; } } @@ -462,7 +455,6 @@ export const validateTracesAndBlock = ( requestText(blockRequest), requestText(tracesRequest), ]; - error.stack = undefined; throw error; } } @@ -477,7 +469,6 @@ export const validateTracesAndBlock = ( requestText(blockRequest), requestText(tracesRequest), ]; - error.stack = undefined; throw error; } }; @@ -514,7 +505,6 @@ export const validateReceiptsAndBlock = ( requestText(blockRequest), requestText(receiptsRequest), ]; - error.stack = undefined; throw error; } @@ -527,7 +517,6 @@ export const validateReceiptsAndBlock = ( requestText(blockRequest), requestText(receiptsRequest), ]; - error.stack = undefined; throw error; } @@ -541,7 +530,6 @@ export const validateReceiptsAndBlock = ( requestText(blockRequest), requestText(receiptsRequest), ]; - error.stack = undefined; throw error; } else if (transaction.hash !== receipt.transactionHash) { const error = new RpcProviderError( @@ -552,7 +540,6 @@ export const validateReceiptsAndBlock = ( requestText(blockRequest), requestText(receiptsRequest), ]; - error.stack = undefined; throw error; } } @@ -569,7 +556,6 @@ export const validateReceiptsAndBlock = ( requestText(blockRequest), requestText(receiptsRequest), ]; - error.stack = undefined; throw error; } }; @@ -624,7 +610,6 @@ export const standardizeBlock = < "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (block.number === undefined) { @@ -635,7 +620,6 @@ export const standardizeBlock = < "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (block.timestamp === undefined) { @@ -646,7 +630,6 @@ export const standardizeBlock = < "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (block.logsBloom === undefined) { @@ -657,7 +640,6 @@ export const standardizeBlock = < "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (block.parentHash === undefined) { @@ -668,7 +650,6 @@ export const standardizeBlock = < "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } @@ -721,7 +702,6 @@ export const standardizeBlock = < "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (hexToBigInt(block.timestamp) > PG_BIGINT_MAX) { @@ -732,7 +712,6 @@ export const standardizeBlock = < "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } @@ -798,7 +777,6 @@ export const standardizeTransactions = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } else { transactionIds.add(transaction.transactionIndex); @@ -813,7 +791,6 @@ export const standardizeTransactions = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (transaction.transactionIndex === undefined) { @@ -824,7 +801,6 @@ export const standardizeTransactions = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (transaction.blockNumber === undefined) { @@ -835,7 +811,6 @@ export const standardizeTransactions = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (transaction.blockHash === undefined) { @@ -846,7 +821,6 @@ export const standardizeTransactions = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (transaction.from === undefined) { @@ -857,7 +831,6 @@ export const standardizeTransactions = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } @@ -901,7 +874,6 @@ export const standardizeTransactions = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (hexToBigInt(transaction.transactionIndex) > BigInt(PG_INTEGER_MAX)) { @@ -912,7 +884,6 @@ export const standardizeTransactions = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (hexToBigInt(transaction.nonce) > BigInt(PG_INTEGER_MAX)) { @@ -923,7 +894,6 @@ export const standardizeTransactions = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } } @@ -962,7 +932,6 @@ export const standardizeLogs = ( ]; console.log(logs); - error.stack = undefined; throw error; } else { logIds.add(`${log.blockNumber}_${log.logIndex}`); @@ -977,7 +946,6 @@ export const standardizeLogs = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (log.logIndex === undefined) { @@ -988,7 +956,6 @@ export const standardizeLogs = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (log.blockHash === undefined) { @@ -999,7 +966,6 @@ export const standardizeLogs = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (log.address === undefined) { @@ -1010,7 +976,6 @@ export const standardizeLogs = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (log.topics === undefined) { @@ -1021,7 +986,6 @@ export const standardizeLogs = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (log.data === undefined) { @@ -1032,7 +996,6 @@ export const standardizeLogs = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (log.transactionHash === undefined) { @@ -1055,7 +1018,6 @@ export const standardizeLogs = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (hexToBigInt(log.transactionIndex) > BigInt(PG_INTEGER_MAX)) { @@ -1066,7 +1028,6 @@ export const standardizeLogs = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (hexToBigInt(log.logIndex) > BigInt(PG_INTEGER_MAX)) { @@ -1077,7 +1038,6 @@ export const standardizeLogs = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } } @@ -1114,7 +1074,6 @@ export const standardizeTrace = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (trace.trace.type === undefined) { @@ -1125,7 +1084,6 @@ export const standardizeTrace = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (trace.trace.from === undefined) { @@ -1136,7 +1094,6 @@ export const standardizeTrace = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (trace.trace.input === undefined) { @@ -1147,7 +1104,6 @@ export const standardizeTrace = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } @@ -1203,7 +1159,6 @@ export const standardizeTransactionReceipts = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } else { receiptIds.add(receipt.transactionHash); @@ -1218,7 +1173,6 @@ export const standardizeTransactionReceipts = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (receipt.blockNumber === undefined) { @@ -1229,7 +1183,6 @@ export const standardizeTransactionReceipts = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (receipt.transactionHash === undefined) { @@ -1240,7 +1193,6 @@ export const standardizeTransactionReceipts = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (receipt.transactionIndex === undefined) { @@ -1251,7 +1203,6 @@ export const standardizeTransactionReceipts = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (receipt.from === undefined) { @@ -1262,7 +1213,6 @@ export const standardizeTransactionReceipts = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (receipt.status === undefined) { @@ -1273,7 +1223,6 @@ export const standardizeTransactionReceipts = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } @@ -1314,7 +1263,6 @@ export const standardizeTransactionReceipts = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } if (hexToBigInt(receipt.transactionIndex) > BigInt(PG_INTEGER_MAX)) { @@ -1325,7 +1273,6 @@ export const standardizeTransactionReceipts = ( "Please report this error to the RPC operator.", requestText(request), ]; - error.stack = undefined; throw error; } } From 25ba81d37a1832480d1ec024d2570db8d4b319c5 Mon Sep 17 00:00:00 2001 From: Kyle Scott Date: Tue, 13 Jan 2026 11:27:44 -0500 Subject: [PATCH 04/11] more error handling --- packages/core/src/bin/commands/dev.ts | 19 ++- packages/core/src/bin/utils/exit.ts | 34 ++-- packages/core/src/build/index.ts | 2 +- packages/core/src/build/stacktrace.ts | 2 +- packages/core/src/database/index.ts | 4 +- .../core/src/database/queryBuilder.test.ts | 7 + packages/core/src/database/queryBuilder.ts | 149 ++++++++++++------ packages/core/src/indexing-store/cache.ts | 11 +- packages/core/src/indexing-store/index.ts | 7 +- packages/core/src/indexing/index.ts | 13 +- packages/core/src/internal/errors.ts | 75 +++++++-- packages/core/src/internal/logger.ts | 3 +- packages/core/src/rpc/actions.ts | 108 ++++++------- packages/core/src/rpc/index.ts | 91 +++++------ packages/core/src/runtime/historical.ts | 41 ++--- packages/utils/src/getLogsRetryHelper.ts | 4 +- 16 files changed, 353 insertions(+), 217 deletions(-) diff --git a/packages/core/src/bin/commands/dev.ts b/packages/core/src/bin/commands/dev.ts index 5e33cc97b..98449cf40 100644 --- a/packages/core/src/bin/commands/dev.ts +++ b/packages/core/src/bin/commands/dev.ts @@ -3,7 +3,11 @@ import path from "node:path"; import { createBuild } from "@/build/index.js"; import { type Database, createDatabase } from "@/database/index.js"; import type { Common } from "@/internal/common.js"; -import { NonRetryableUserError, ShutdownError } from "@/internal/errors.js"; +import { + BaseError, + NonRetryableUserError, + ShutdownError, +} from "@/internal/errors.js"; import { createLogger } from "@/internal/logger.js"; import { MetricsService } from "@/internal/metrics.js"; import { buildOptions } from "@/internal/options.js"; @@ -457,19 +461,20 @@ export async function dev({ cliOptions }: { cliOptions: CliOptions }) { }); process.on("unhandledRejection", (error: Error) => { if (error instanceof ShutdownError) return; - if (error instanceof NonRetryableUserError) { + if (error instanceof BaseError) { common.logger.error({ - msg: "unhandledRejection", - error, + msg: `unhandledRejection: ${error.name}`, }); - - buildQueue.clear(); - buildQueue.add({ status: "error", kind: "indexing", error }); } else { common.logger.error({ msg: "unhandledRejection", error, }); + } + if (error instanceof NonRetryableUserError) { + buildQueue.clear(); + buildQueue.add({ status: "error", kind: "indexing", error }); + } else { exit({ code: 75 }); } }); diff --git a/packages/core/src/bin/utils/exit.ts b/packages/core/src/bin/utils/exit.ts index b7f1b1a35..95d147ac2 100644 --- a/packages/core/src/bin/utils/exit.ts +++ b/packages/core/src/bin/utils/exit.ts @@ -1,7 +1,11 @@ import os from "node:os"; import readline from "node:readline"; import type { Common } from "@/internal/common.js"; -import { NonRetryableUserError, ShutdownError } from "@/internal/errors.js"; +import { + BaseError, + NonRetryableUserError, + ShutdownError, +} from "@/internal/errors.js"; import type { Options } from "@/internal/options.js"; const SHUTDOWN_GRACE_PERIOD_MS = 5_000; @@ -84,10 +88,16 @@ export const createExit = ({ if (options.command !== "dev") { process.on("uncaughtException", (error: Error) => { if (error instanceof ShutdownError) return; - common.logger.error({ - msg: "uncaughtException", - error, - }); + if (error instanceof BaseError) { + common.logger.error({ + msg: `unhandledRejection: ${error.name}`, + }); + } else { + common.logger.error({ + msg: "unhandledRejection", + error, + }); + } if (error instanceof NonRetryableUserError) { exit({ code: 1 }); } else { @@ -96,10 +106,16 @@ export const createExit = ({ }); process.on("unhandledRejection", (error: Error) => { if (error instanceof ShutdownError) return; - common.logger.error({ - msg: "unhandledRejection", - error, - }); + if (error instanceof BaseError) { + common.logger.error({ + msg: `unhandledRejection: ${error.name}`, + }); + } else { + common.logger.error({ + msg: "unhandledRejection", + error, + }); + } if (error instanceof NonRetryableUserError) { exit({ code: 1 }); } else { diff --git a/packages/core/src/build/index.ts b/packages/core/src/build/index.ts index 4996bfd0d..553f1a19d 100644 --- a/packages/core/src/build/index.ts +++ b/packages/core/src/build/index.ts @@ -280,7 +280,7 @@ export const createBuild = async ({ try { const contents = fs.readFileSync(file, "utf-8"); hash.update(contents); - } catch (e) { + } catch { common.logger.warn({ msg: "Unable to read file", file }); hash.update(file); } diff --git a/packages/core/src/build/stacktrace.ts b/packages/core/src/build/stacktrace.ts index 9667e7eaa..9357d66bd 100644 --- a/packages/core/src/build/stacktrace.ts +++ b/packages/core/src/build/stacktrace.ts @@ -131,7 +131,7 @@ export function parseViteNodeError(file: string, error: Error): ViteNodeError { // This can throw with "Cannot set property message of [object Object] which has only a getter" try { resolvedError.message = `Error while ${verb} ${file}: ${resolvedError.message}`; - } catch (e) {} + } catch {} return resolvedError; } diff --git a/packages/core/src/database/index.ts b/packages/core/src/database/index.ts index 467f3f0d5..04d2dc593 100644 --- a/packages/core/src/database/index.ts +++ b/packages/core/src/database/index.ts @@ -48,7 +48,7 @@ import { dropLiveQueryTriggers, dropTriggers, } from "./actions.js"; -import { type QB, createQB, parseDbError } from "./queryBuilder.js"; +import { type QB, createQB, parseQBError } from "./queryBuilder.js"; export type Database = { driver: PostgresDriver | PGliteDriver; @@ -454,7 +454,7 @@ export const createDatabase = ({ return; } catch (_error) { - const error = parseDbError(_error); + const error = parseQBError(_error as Error); if (common.shutdown.isKilled) { throw new ShutdownError(); diff --git a/packages/core/src/database/queryBuilder.test.ts b/packages/core/src/database/queryBuilder.test.ts index eab235d90..460009874 100644 --- a/packages/core/src/database/queryBuilder.test.ts +++ b/packages/core/src/database/queryBuilder.test.ts @@ -101,6 +101,13 @@ test("QB transaction retries error", async () => { // BEGIN, BEGIN, SELECT, ROLLBACK, BEGIN, SELECT, COMMIT expect(querySpy).toHaveBeenCalledTimes(7); + // unrecognized errors are propagated + await expect( + qb.transaction({ label: "test1" }, async () => { + throw new Error("i'm an error"); + }), + ).rejects.toThrow("i'm an error"); + connection.release(); }); diff --git a/packages/core/src/database/queryBuilder.ts b/packages/core/src/database/queryBuilder.ts index 163e3993f..a80bacf74 100644 --- a/packages/core/src/database/queryBuilder.ts +++ b/packages/core/src/database/queryBuilder.ts @@ -1,6 +1,11 @@ import crypto from "node:crypto"; import type { Common } from "@/internal/common.js"; -import { BaseError } from "@/internal/errors.js"; +import { + BaseError, + TransactionCallbackError, + TransactionControlError, + TransactionStatementError, +} from "@/internal/errors.js"; import { BigIntSerializationError, CheckConstraintError, @@ -59,6 +64,8 @@ type TransactionQB< ): Promise; }; +// TODO(kyle) handle malformed queries + /** * Query builder with built-in retry logic, logging, and metrics. */ @@ -87,33 +94,35 @@ export type QB< | { $dialect: "postgres"; $client: pg.Pool | pg.PoolClient } ); -export const parseDbError = (error: any): Error => { - const stack = error.stack; +export const parseQBError = (error: Error): Error => { + // TODO(kyle) how to know if the error is a query builder error? - if (error instanceof BaseError) { - return error; - } + // TODO(kyle) do we need this? + if (error instanceof BaseError) return error; if (error?.message?.includes("violates not-null constraint")) { - error = new NotNullConstraintError(error.message); + return new NotNullConstraintError(undefined, { cause: error }); } else if (error?.message?.includes("violates unique constraint")) { - error = new UniqueConstraintError(error.message); + return new UniqueConstraintError(undefined, { cause: error }); } else if (error?.message?.includes("violates check constraint")) { - error = new CheckConstraintError(error.message); + return new CheckConstraintError(undefined, { cause: error }); } else if ( // nodejs error message error?.message?.includes("Do not know how to serialize a BigInt") || // bun error message error?.message?.includes("cannot serialize BigInt") ) { - error = new BigIntSerializationError(error.message); - error.meta.push( + const bigIntSerializationError = new BigIntSerializationError(undefined, { + cause: error, + }); + bigIntSerializationError.meta.push( "Hint:\n The JSON column type does not support BigInt values. Use the replaceBigInts() helper function before inserting into the database. Docs: https://ponder.sh/docs/api-reference/ponder-utils#replacebigints", ); + return bigIntSerializationError; } else if (error?.message?.includes("does not exist")) { - error = new NonRetryableUserError(error.message); + return new NonRetryableUserError(error.message); } else if (error?.message?.includes("already exists")) { - error = new NonRetryableUserError(error.message); + return new NonRetryableUserError(error.message); } else if ( error?.message?.includes( "terminating connection due to administrator command", @@ -125,11 +134,9 @@ export const parseDbError = (error: any): Error => { error?.message?.includes("ETIMEDOUT") || error?.message?.includes("timeout exceeded when trying to connect") ) { - error = new DbConnectionError(error.message); + return new DbConnectionError(error.message); } - error.stack = stack; - return error; }; @@ -209,8 +216,8 @@ export const createQB = < } return result; - } catch (e) { - const error = parseDbError(e); + } catch (_error) { + let error = parseQBError(_error as Error); if (common.shutdown.isKilled) { throw new ShutdownError(); @@ -231,17 +238,14 @@ export const createQB = < firstError = error; } - // Two types of transaction environments - // 1. Inside callback (running user statements or control flow statements): Throw error, retry - // later. We want the error bubbled up out of the callback, so the transaction is properly rolled back. - // 2. Outside callback (running entire transaction, user statements + control flow statements): Retry immediately. + // Three types of query environments + // 1. Query outside of a transaction: Retry immediately. + // 2. Query inside of a transaction: Throw error, retry later. + // We want the error bubbled up out of the transaction callback scope, so the + // so the control flow can rollback the transaction. + // 3. Transaction callback: Retry immediately if the error was from #2 or from control statements, else throw error. - if (isTransaction) { - if (error instanceof NonRetryableUserError) { - throw error; - } - } else if (isTransactionStatement) { - // Transaction statements are not immediately retried, so the transaction will be properly rolled back. + if (isTransaction === false && isTransactionStatement) { logger.warn({ msg: "Failed database query", query: label, @@ -249,7 +253,10 @@ export const createQB = < duration: endClock(), error, }); - throw error; + // Transaction statements are not immediately retried, so the transaction will be properly rolled back. + throw new TransactionStatementError(undefined, { cause: error }); + } else if (error instanceof TransactionCallbackError) { + throw error.cause; } else if (error instanceof NonRetryableUserError) { logger.warn({ msg: "Failed database query", @@ -261,6 +268,14 @@ export const createQB = < throw error; } + if ( + isTransaction && + error instanceof TransactionStatementError === false && + error instanceof TransactionCallbackError === false + ) { + error = new TransactionControlError(undefined, { cause: error }); + } + if (i === RETRY_COUNT) { logger.warn({ msg: "Failed database query", @@ -365,8 +380,16 @@ export const createQB = < } }; - const result = await callback(tx); - return result; + try { + const result = await callback(tx); + return result; + } catch (error) { + if (error instanceof TransactionStatementError) { + throw error; + } else { + throw new TransactionCallbackError({ cause: error as Error }); + } + } }, config), { isTransaction: true, @@ -447,8 +470,16 @@ export const createQB = < } }; - const result = await callback(tx); - return result; + try { + const result = await callback(tx); + return result; + } catch (error) { + if (error instanceof TransactionStatementError) { + throw error; + } else { + throw new TransactionCallbackError({ cause: error as Error }); + } + } }, config), { label, @@ -478,11 +509,26 @@ export const createQB = < { logger?: Logger } | undefined, ]; - // @ts-expect-error - return retryLogMetricErrorWrap(() => callback(db), { - isTransactionStatement: true, - logger: context?.logger ?? common.logger, - }); + return retryLogMetricErrorWrap( + async () => { + try { + // @ts-expect-error + const result = await callback(db); + return result; + } catch (error) { + if (error instanceof TransactionStatementError) { + throw error; + } else { + throw new TransactionCallbackError({ cause: error as Error }); + } + } + }, + { + isTransaction: false, + isTransactionStatement: true, + logger: context?.logger ?? common.logger, + }, + ); } else { const [{ label }, callback, context] = args as [ { label: string }, @@ -496,12 +542,27 @@ export const createQB = < { logger?: Logger } | undefined, ]; - // @ts-expect-error - return retryLogMetricErrorWrap(() => callback(db), { - label, - isTransactionStatement: true, - logger: context?.logger ?? common.logger, - }); + return retryLogMetricErrorWrap( + async () => { + try { + // @ts-expect-error + const result = await callback(db); + return result; + } catch (error) { + if (error instanceof TransactionStatementError) { + throw error; + } else { + throw new TransactionCallbackError({ cause: error as Error }); + } + } + }, + { + label, + isTransaction: false, + isTransactionStatement: true, + logger: context?.logger ?? common.logger, + }, + ); } }; } diff --git a/packages/core/src/indexing-store/cache.ts b/packages/core/src/indexing-store/cache.ts index bd90efd6a..5ed6b231d 100644 --- a/packages/core/src/indexing-store/cache.ts +++ b/packages/core/src/indexing-store/cache.ts @@ -285,10 +285,8 @@ export const getCopyHelper = (qb: QB, chainId?: number) => { .query(`COPY ${target} FROM '/dev/blob'`, [], { blob: new Blob([text]), }) - // Note: `TransactionError` is applied because the query - // uses the low-level `$client.query` method. .catch((error) => { - throw new CopyFlushError(error.message); + throw new CopyFlushError(undefined, { cause: error as Error }); }); }; } else { @@ -307,7 +305,7 @@ export const getCopyHelper = (qb: QB, chainId?: number) => { copyStream.write(text); copyStream.end(); }).catch((error) => { - throw new CopyFlushError(error.message); + throw new CopyFlushError(undefined, { cause: error as Error }); }); }; } @@ -829,8 +827,9 @@ export const createIndexingCache = ({ ); if (result.status === "error") { - error = new DelayedInsertError(result.error.message); - error.stack = undefined; + error = new DelayedInsertError(undefined, { + cause: result.error as Error, + }); addErrorMeta( error, diff --git a/packages/core/src/indexing-store/index.ts b/packages/core/src/indexing-store/index.ts index 5e7c32a8c..fc2bd1b1c 100644 --- a/packages/core/src/indexing-store/index.ts +++ b/packages/core/src/indexing-store/index.ts @@ -2,7 +2,6 @@ import type { QB } from "@/database/queryBuilder.js"; import { onchain } from "@/drizzle/onchain.js"; import type { Common } from "@/internal/common.js"; import { - DbConnectionError, InvalidStoreAccessError, InvalidStoreMethodError, NonRetryableUserError, @@ -604,11 +603,7 @@ export const createIndexingStore = ({ return result; }); } catch (error) { - if (error instanceof DbConnectionError) { - throw error; - } - - throw new RawSqlError((error as Error).message); + throw new RawSqlError(undefined, { cause: error as Error }); } finally { common.metrics.ponder_indexing_store_raw_sql_duration.observe( endClock(), diff --git a/packages/core/src/indexing/index.ts b/packages/core/src/indexing/index.ts index 4a1e9fba0..ef5051310 100644 --- a/packages/core/src/indexing/index.ts +++ b/packages/core/src/indexing/index.ts @@ -4,7 +4,6 @@ import type { IndexingStore } from "@/indexing-store/index.js"; import type { CachedViemClient } from "@/indexing/client.js"; import type { Common } from "@/internal/common.js"; import { - BaseError, IndexingFunctionError, InvalidEventAccessError, ShutdownError, @@ -256,11 +255,7 @@ export const createIndexing = ({ common.metrics.hasError = true; - if (error instanceof BaseError === false) { - error = new IndexingFunctionError(error.message); - } - - throw error; + throw new IndexingFunctionError(undefined, { cause: error as Error }); } }; @@ -332,11 +327,7 @@ export const createIndexing = ({ common.metrics.hasError = true; - if (error instanceof BaseError === false) { - error = new IndexingFunctionError(error.message); - } - - throw error; + throw new IndexingFunctionError(undefined, { cause: error as Error }); } }; diff --git a/packages/core/src/internal/errors.ts b/packages/core/src/internal/errors.ts index 3cdefd49d..f247fd136 100644 --- a/packages/core/src/internal/errors.ts +++ b/packages/core/src/internal/errors.ts @@ -115,14 +115,14 @@ export class DbConnectionError extends RetryableError { } } -export class TransactionStatementError extends RetryableError { - override name = "TransactionStatementError"; +// export class TransactionStatementError extends RetryableError { +// override name = "TransactionStatementError"; - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { - super(message, { cause }); - Object.setPrototypeOf(this, TransactionStatementError.prototype); - } -} +// constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { +// super(message, { cause }); +// Object.setPrototypeOf(this, TransactionStatementError.prototype); +// } +// } export class CopyFlushError extends RetryableError { override name = "CopyFlushError"; @@ -201,12 +201,67 @@ export class IndexingFunctionError extends NonRetryableUserError { } } -export class RpcProviderError extends BaseError { - override name = "RpcProviderError"; +/** + * @dev All JSON-RPC request errors are retryable. + */ +export class RpcRequestError< + cause extends Error | undefined = undefined, +> extends RetryableError { + override name = "RpcRequestError"; + override cause: cause; + + constructor(message?: string | undefined, { cause }: { cause?: cause } = {}) { + super(message, { cause }); + // @ts-ignore + this.cause = cause; + Object.setPrototypeOf(this, RpcRequestError.prototype); + } +} + +export class QueryBuilderRetryableError extends RetryableError { + override name = "QueryBuilderRetryableError"; constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { super(message, { cause }); - Object.setPrototypeOf(this, RpcProviderError.prototype); + Object.setPrototypeOf(this, QueryBuilderRetryableError.prototype); + } +} + +export class QueryBuilderNonRetryableError extends NonRetryableUserError { + override name = "QueryBuilderNonRetryableError"; + + constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + super(message, { cause }); + Object.setPrototypeOf(this, QueryBuilderNonRetryableError.prototype); + } +} + +export class TransactionControlError extends QueryBuilderRetryableError { + override name = "TransactionControlError"; + + constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + super(message, { cause }); + Object.setPrototypeOf(this, TransactionControlError.prototype); + } +} + +export class TransactionStatementError extends QueryBuilderRetryableError { + override name = "TransactionStatementError"; + + constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + super(message, { cause }); + Object.setPrototypeOf(this, TransactionStatementError.prototype); + } +} + +export class TransactionCallbackError extends QueryBuilderRetryableError { + override name = "TransactionCallbackError"; + override cause: Error; + + constructor({ cause }: { cause: Error }) { + super(undefined, { cause }); + this.cause = cause; + Object.setPrototypeOf(this, TransactionCallbackError.prototype); } } diff --git a/packages/core/src/internal/logger.ts b/packages/core/src/internal/logger.ts index 691e12581..7ce4e1ddf 100644 --- a/packages/core/src/internal/logger.ts +++ b/packages/core/src/internal/logger.ts @@ -2,6 +2,7 @@ import type { Prettify } from "@/types/utils.js"; import { formatEta } from "@/utils/format.js"; import pc from "picocolors"; import { type DestinationStream, type LevelWithSilent, pino } from "pino"; +import { NonRetryableUserError } from "./errors.js"; export type LogMode = "pretty" | "json"; export type LogLevel = Prettify; @@ -235,7 +236,7 @@ const format = (log: Log) => { } if (log.error) { - if (log.error.stack) { + if (log.error.stack && log.error instanceof NonRetryableUserError) { prettyLog.push(log.error.stack); } else { prettyLog.push(`${log.error.name}: ${log.error.message}`); diff --git a/packages/core/src/rpc/actions.ts b/packages/core/src/rpc/actions.ts index 0d0e4dc52..f943ab071 100644 --- a/packages/core/src/rpc/actions.ts +++ b/packages/core/src/rpc/actions.ts @@ -1,4 +1,4 @@ -import { RpcProviderError } from "@/internal/errors.js"; +import { RpcRequestError } from "@/internal/errors.js"; import type { LightBlock, SyncBlock, @@ -320,7 +320,7 @@ export const validateTransactionsAndBlock = ( ) => { for (const [index, transaction] of block.transactions.entries()) { if (block.hash !== transaction.blockHash) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Inconsistent RPC response data. The transaction at index ${index} of the 'block.transactions' array has a 'transaction.blockHash' of ${transaction.blockHash}, but the block itself has a 'block.hash' of ${block.hash}.`, ); error.meta = [ @@ -331,7 +331,7 @@ export const validateTransactionsAndBlock = ( } if (block.number !== transaction.blockNumber) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Inconsistent RPC response data. The transaction at index ${index} of the 'block.transactions' array has a 'transaction.blockNumber' of ${transaction.blockNumber} (${hexToNumber(transaction.blockNumber)}), but the block itself has a 'block.number' of ${block.number} (${hexToNumber(block.number)}).`, ); error.meta = [ @@ -359,7 +359,7 @@ export const validateLogsAndBlock = ( >, ) => { if (block.logsBloom !== zeroLogsBloom && logs.length === 0) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Inconsistent RPC response data. The logs array has length 0, but the associated block has a non-empty 'block.logsBloom'.`, ); error.meta = [ @@ -379,7 +379,7 @@ export const validateLogsAndBlock = ( for (const log of logs) { if (block.hash !== log.blockHash) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Inconsistent RPC response data. The log with 'logIndex' ${log.logIndex} (${hexToNumber(log.logIndex)}) has a 'log.blockHash' of ${log.blockHash}, but the associated block has a 'block.hash' of ${block.hash}.`, ); error.meta = [ @@ -391,7 +391,7 @@ export const validateLogsAndBlock = ( } if (block.number !== log.blockNumber) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Inconsistent RPC response data. The log with 'logIndex' ${log.logIndex} (${hexToNumber(log.logIndex)}) has a 'log.blockNumber' of ${log.blockNumber} (${hexToNumber(log.blockNumber)}), but the associated block has a 'block.number' of ${block.number} (${hexToNumber(block.number)}).`, ); error.meta = [ @@ -405,7 +405,7 @@ export const validateLogsAndBlock = ( if (log.transactionHash !== zeroHash) { const transaction = transactionByIndex.get(log.transactionIndex); if (transaction === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Inconsistent RPC response data. The log with 'logIndex' ${log.logIndex} (${hexToNumber(log.logIndex)}) has a 'log.transactionIndex' of ${log.transactionIndex} (${hexToNumber(log.transactionIndex)}), but the associated 'block.transactions' array does not contain a transaction matching that 'transactionIndex'.`, ); error.meta = [ @@ -415,7 +415,7 @@ export const validateLogsAndBlock = ( ]; throw error; } else if (transaction.hash !== log.transactionHash) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Inconsistent RPC response data. The log with 'logIndex' ${log.logIndex} (${hexToNumber(log.logIndex)}) matches a transaction in the associated 'block.transactions' array by 'transactionIndex' ${log.transactionIndex} (${hexToNumber(log.transactionIndex)}), but the log has a 'log.transactionHash' of ${log.transactionHash} while the transaction has a 'transaction.hash' of ${transaction.hash}.`, ); error.meta = [ @@ -447,7 +447,7 @@ export const validateTracesAndBlock = ( const transactionHashes = new Set(block.transactions.map((t) => t.hash)); for (const [index, trace] of traces.entries()) { if (transactionHashes.has(trace.transactionHash) === false) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Inconsistent RPC response data. The top-level trace at array index ${index} has a 'transactionHash' of ${trace.transactionHash}, but the associated 'block.transactions' array does not contain a transaction matching that hash.`, ); error.meta = [ @@ -461,7 +461,7 @@ export const validateTracesAndBlock = ( // Use the fact that any transaction produces a trace to validate. if (block.transactions.length !== 0 && traces.length === 0) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Inconsistent RPC response data. The traces array has length 0, but the associated 'block.transactions' array has length ${block.transactions.length}.`, ); error.meta = [ @@ -497,7 +497,7 @@ export const validateReceiptsAndBlock = ( for (const [index, receipt] of receipts.entries()) { if (block.hash !== receipt.blockHash) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Inconsistent RPC response data. The receipt at array index ${index} has a 'receipt.blockHash' of ${receipt.blockHash}, but the associated block has a 'block.hash' of ${block.hash}.`, ); error.meta = [ @@ -509,7 +509,7 @@ export const validateReceiptsAndBlock = ( } if (block.number !== receipt.blockNumber) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Inconsistent RPC response data. The receipt at array index ${index} has a 'receipt.blockNumber' of ${receipt.blockNumber} (${hexToNumber(receipt.blockNumber)}), but the associated block has a 'block.number' of ${block.number} (${hexToNumber(block.number)}).`, ); error.meta = [ @@ -522,7 +522,7 @@ export const validateReceiptsAndBlock = ( const transaction = transactionByIndex.get(receipt.transactionIndex); if (transaction === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Inconsistent RPC response data. The receipt at array index ${index} has a 'receipt.transactionIndex' of ${receipt.transactionIndex} (${hexToNumber(receipt.transactionIndex)}), but the associated 'block.transactions' array does not contain a transaction matching that 'transactionIndex'.`, ); error.meta = [ @@ -532,7 +532,7 @@ export const validateReceiptsAndBlock = ( ]; throw error; } else if (transaction.hash !== receipt.transactionHash) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Inconsistent RPC response data. The receipt at array index ${index} matches a transaction in the associated 'block.transactions' array by 'transactionIndex' ${receipt.transactionIndex} (${hexToNumber(receipt.transactionIndex)}), but the receipt has a 'receipt.transactionHash' of ${receipt.transactionHash} while the transaction has a 'transaction.hash' of ${transaction.hash}.`, ); error.meta = [ @@ -548,7 +548,7 @@ export const validateReceiptsAndBlock = ( receiptsRequest.method === "eth_getBlockReceipts" && block.transactions.length !== receipts.length ) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Inconsistent RPC response data. The receipts array has length ${receipts.length}, but the associated 'block.transactions' array has length ${block.transactions.length}.`, ); error.meta = [ @@ -603,7 +603,7 @@ export const standardizeBlock = < ): block extends SyncBlock ? SyncBlock : SyncBlockHeader => { // required properties if (block.hash === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'block.hash' is a required property", ); error.meta = [ @@ -613,7 +613,7 @@ export const standardizeBlock = < throw error; } if (block.number === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'block.number' is a required property", ); error.meta = [ @@ -623,7 +623,7 @@ export const standardizeBlock = < throw error; } if (block.timestamp === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'block.timestamp' is a required property", ); error.meta = [ @@ -633,7 +633,7 @@ export const standardizeBlock = < throw error; } if (block.logsBloom === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'block.logsBloom' is a required property", ); error.meta = [ @@ -643,7 +643,7 @@ export const standardizeBlock = < throw error; } if (block.parentHash === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'block.parentHash' is a required property", ); error.meta = [ @@ -695,7 +695,7 @@ export const standardizeBlock = < } if (hexToBigInt(block.number) > PG_BIGINT_MAX) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Invalid RPC response: 'block.number' (${hexToBigInt(block.number)}) is larger than the maximum allowed value (${PG_BIGINT_MAX}).`, ); error.meta = [ @@ -705,7 +705,7 @@ export const standardizeBlock = < throw error; } if (hexToBigInt(block.timestamp) > PG_BIGINT_MAX) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Invalid RPC response: 'block.timestamp' (${hexToBigInt(block.timestamp)}) is larger than the maximum allowed value (${PG_BIGINT_MAX}).`, ); error.meta = [ @@ -770,7 +770,7 @@ export const standardizeTransactions = ( for (const transaction of transactions) { if (transactionIds.has(transaction.transactionIndex)) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Inconsistent RPC response data. The 'block.transactions' array contains two objects with a 'transactionIndex' of ${transaction.transactionIndex} (${hexToNumber(transaction.transactionIndex)}).`, ); error.meta = [ @@ -784,7 +784,7 @@ export const standardizeTransactions = ( // required properties if (transaction.hash === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'transaction.hash' is a required property", ); error.meta = [ @@ -794,7 +794,7 @@ export const standardizeTransactions = ( throw error; } if (transaction.transactionIndex === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'transaction.transactionIndex' is a required property", ); error.meta = [ @@ -804,7 +804,7 @@ export const standardizeTransactions = ( throw error; } if (transaction.blockNumber === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'transaction.blockNumber' is a required property", ); error.meta = [ @@ -814,7 +814,7 @@ export const standardizeTransactions = ( throw error; } if (transaction.blockHash === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'transaction.blockHash' is a required property", ); error.meta = [ @@ -824,7 +824,7 @@ export const standardizeTransactions = ( throw error; } if (transaction.from === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'transaction.from' is a required property", ); error.meta = [ @@ -867,7 +867,7 @@ export const standardizeTransactions = ( } if (hexToBigInt(transaction.blockNumber) > PG_BIGINT_MAX) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Invalid RPC response: 'transaction.blockNumber' (${hexToBigInt(transaction.blockNumber)}) is larger than the maximum allowed value (${PG_BIGINT_MAX}).`, ); error.meta = [ @@ -877,7 +877,7 @@ export const standardizeTransactions = ( throw error; } if (hexToBigInt(transaction.transactionIndex) > BigInt(PG_INTEGER_MAX)) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Invalid RPC response: 'transaction.transactionIndex' (${hexToBigInt(transaction.transactionIndex)}) is larger than the maximum allowed value (${PG_INTEGER_MAX}).`, ); error.meta = [ @@ -887,7 +887,7 @@ export const standardizeTransactions = ( throw error; } if (hexToBigInt(transaction.nonce) > BigInt(PG_INTEGER_MAX)) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Invalid RPC response: 'transaction.nonce' (${hexToBigInt(transaction.nonce)}) is larger than the maximum allowed value (${PG_INTEGER_MAX}).`, ); error.meta = [ @@ -923,7 +923,7 @@ export const standardizeLogs = ( const logIds = new Set(); for (const log of logs) { if (logIds.has(`${log.blockNumber}_${log.logIndex}`)) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Inconsistent RPC response data. The logs array contains two objects with 'blockNumber' ${log.blockNumber} (${hexToNumber(log.blockNumber)}) and 'logIndex' ${log.logIndex} (${hexToNumber(log.logIndex)}).`, ); error.meta = [ @@ -939,7 +939,7 @@ export const standardizeLogs = ( // required properties if (log.blockNumber === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'log.blockNumber' is a required property", ); error.meta = [ @@ -949,7 +949,7 @@ export const standardizeLogs = ( throw error; } if (log.logIndex === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'log.logIndex' is a required property", ); error.meta = [ @@ -959,7 +959,7 @@ export const standardizeLogs = ( throw error; } if (log.blockHash === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'log.blockHash' is a required property", ); error.meta = [ @@ -969,7 +969,7 @@ export const standardizeLogs = ( throw error; } if (log.address === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'log.address' is a required property", ); error.meta = [ @@ -979,7 +979,7 @@ export const standardizeLogs = ( throw error; } if (log.topics === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'log.topics' is a required property", ); error.meta = [ @@ -989,7 +989,7 @@ export const standardizeLogs = ( throw error; } if (log.data === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'log.data' is a required property", ); error.meta = [ @@ -1011,7 +1011,7 @@ export const standardizeLogs = ( } if (hexToBigInt(log.blockNumber) > PG_BIGINT_MAX) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Invalid RPC response: 'log.blockNumber' (${hexToBigInt(log.blockNumber)}) is larger than the maximum allowed value (${PG_BIGINT_MAX}).`, ); error.meta = [ @@ -1021,7 +1021,7 @@ export const standardizeLogs = ( throw error; } if (hexToBigInt(log.transactionIndex) > BigInt(PG_INTEGER_MAX)) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Invalid RPC response: 'log.transactionIndex' (${hexToBigInt(log.transactionIndex)}) is larger than the maximum allowed value (${PG_INTEGER_MAX}).`, ); error.meta = [ @@ -1031,7 +1031,7 @@ export const standardizeLogs = ( throw error; } if (hexToBigInt(log.logIndex) > BigInt(PG_INTEGER_MAX)) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Invalid RPC response: 'log.logIndex' (${hexToBigInt(log.logIndex)}) is larger than the maximum allowed value (${PG_INTEGER_MAX}).`, ); error.meta = [ @@ -1067,7 +1067,7 @@ export const standardizeTrace = ( ): SyncTrace => { // required properties if (trace.transactionHash === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'trace.transactionHash' is a required property", ); error.meta = [ @@ -1077,7 +1077,7 @@ export const standardizeTrace = ( throw error; } if (trace.trace.type === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'trace.type' is a required property", ); error.meta = [ @@ -1087,7 +1087,7 @@ export const standardizeTrace = ( throw error; } if (trace.trace.from === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'trace.from' is a required property", ); error.meta = [ @@ -1097,7 +1097,7 @@ export const standardizeTrace = ( throw error; } if (trace.trace.input === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'trace.input' is a required property", ); error.meta = [ @@ -1152,7 +1152,7 @@ export const standardizeTransactionReceipts = ( const receiptIds = new Set(); for (const receipt of receipts) { if (receiptIds.has(receipt.transactionHash)) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Inconsistent RPC response data. The receipts array contains two objects with a 'transactionHash' of ${receipt.transactionHash}.`, ); error.meta = [ @@ -1166,7 +1166,7 @@ export const standardizeTransactionReceipts = ( // required properties if (receipt.blockHash === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'receipt.blockHash' is a required property", ); error.meta = [ @@ -1176,7 +1176,7 @@ export const standardizeTransactionReceipts = ( throw error; } if (receipt.blockNumber === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'receipt.blockNumber' is a required property", ); error.meta = [ @@ -1186,7 +1186,7 @@ export const standardizeTransactionReceipts = ( throw error; } if (receipt.transactionHash === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'receipt.transactionHash' is a required property", ); error.meta = [ @@ -1196,7 +1196,7 @@ export const standardizeTransactionReceipts = ( throw error; } if (receipt.transactionIndex === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'receipt.transactionIndex' is a required property", ); error.meta = [ @@ -1206,7 +1206,7 @@ export const standardizeTransactionReceipts = ( throw error; } if (receipt.from === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'receipt.from' is a required property", ); error.meta = [ @@ -1216,7 +1216,7 @@ export const standardizeTransactionReceipts = ( throw error; } if (receipt.status === undefined) { - const error = new RpcProviderError( + const error = new RpcRequestError( "Invalid RPC response: 'receipt.status' is a required property", ); error.meta = [ @@ -1256,7 +1256,7 @@ export const standardizeTransactionReceipts = ( } if (hexToBigInt(receipt.blockNumber) > PG_BIGINT_MAX) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Invalid RPC response: 'receipt.blockNumber' (${hexToBigInt(receipt.blockNumber)}) is larger than the maximum allowed value (${PG_BIGINT_MAX}).`, ); error.meta = [ @@ -1266,7 +1266,7 @@ export const standardizeTransactionReceipts = ( throw error; } if (hexToBigInt(receipt.transactionIndex) > BigInt(PG_INTEGER_MAX)) { - const error = new RpcProviderError( + const error = new RpcRequestError( `Invalid RPC response: 'receipt.transactionIndex' (${hexToBigInt(receipt.transactionIndex)}) is larger than the maximum allowed value (${PG_INTEGER_MAX}).`, ); error.meta = [ diff --git a/packages/core/src/rpc/index.ts b/packages/core/src/rpc/index.ts index 561d3b564..2e2f36ec1 100644 --- a/packages/core/src/rpc/index.ts +++ b/packages/core/src/rpc/index.ts @@ -1,6 +1,7 @@ import crypto, { type UUID } from "node:crypto"; import url from "node:url"; import type { Common } from "@/internal/common.js"; +import { RpcRequestError } from "@/internal/errors.js"; import type { Logger } from "@/internal/logger.js"; import type { Chain, SyncBlock, SyncBlockHeader } from "@/internal/types.js"; import { eth_getBlockByNumber, standardizeBlock } from "@/rpc/actions.js"; @@ -22,7 +23,6 @@ import { MethodNotSupportedRpcError, ParseRpcError, type PublicRpcSchema, - type RpcError, type RpcTransactionReceipt, TimeoutError, custom, @@ -542,8 +542,10 @@ export const createRpc = ({ bucket.reactivationDelay = INITIAL_REACTIVATION_DELAY; return response as RequestReturnType; - } catch (e) { - const error = e as Error; + } catch (_error) { + const error = new RpcRequestError(undefined, { + cause: _error as Error, + }); common.metrics.ponder_rpc_request_error_total.inc( { method: body.method, chain: chain.name }, @@ -558,7 +560,7 @@ export const createRpc = ({ ) { const getLogsErrorResponse = getLogsRetryHelper({ params: body.params as GetLogsRetryHelperParameters["params"], - error: error as RpcError, + error, }); if (getLogsErrorResponse.shouldRetry) { @@ -571,12 +573,11 @@ export const createRpc = ({ method: body.method, request: JSON.stringify(body), retry_ranges: JSON.stringify(getLogsErrorResponse.ranges), - error: error as Error, + error, }); throw error; } else { - // @ts-ignore error.meta = [ "Tip: Use the ethGetLogsBlockRange option to override the default behavior for this chain", ]; @@ -585,52 +586,44 @@ export const createRpc = ({ addLatency(bucket, endClock(), false); - if ( - // @ts-ignore - error.code === 429 || - // @ts-ignore - error.status === 429 || - error instanceof TimeoutError - ) { - if (bucket.isActive) { - bucket.isActive = false; - bucket.isWarmingUp = false; + if (shouldRateLimit(error.cause) && bucket.isActive) { + bucket.isActive = false; + bucket.isWarmingUp = false; - bucket.rpsLimit = Math.max( - bucket.rpsLimit * RPS_DECREASE_FACTOR, - MIN_RPS, - ); - bucket.consecutiveSuccessfulRequests = 0; + bucket.rpsLimit = Math.max( + bucket.rpsLimit * RPS_DECREASE_FACTOR, + MIN_RPS, + ); + bucket.consecutiveSuccessfulRequests = 0; - common.logger.debug({ - msg: "JSON-RPC provider rate limited", + common.logger.debug({ + msg: "JSON-RPC provider rate limited", + chain: chain.name, + chain_id: chain.id, + hostname: bucket.hostname, + rps_limit: Math.floor(bucket.rpsLimit), + }); + + scheduleBucketActivation(bucket); + + if (buckets.every((b) => b.isActive === false)) { + logger.warn({ + msg: "All JSON-RPC providers are inactive", chain: chain.name, chain_id: chain.id, - hostname: bucket.hostname, - rps_limit: Math.floor(bucket.rpsLimit), }); - - scheduleBucketActivation(bucket); - - if (buckets.every((b) => b.isActive === false)) { - logger.warn({ - msg: "All JSON-RPC providers are inactive", - chain: chain.name, - chain_id: chain.id, - }); - } - - bucket.reactivationDelay = - error instanceof TimeoutError - ? INITIAL_REACTIVATION_DELAY - : Math.min( - bucket.reactivationDelay * BACKOFF_FACTOR, - MAX_REACTIVATION_DELAY, - ); } + + bucket.reactivationDelay = + error.cause instanceof TimeoutError + ? INITIAL_REACTIVATION_DELAY + : Math.min( + bucket.reactivationDelay * BACKOFF_FACTOR, + MAX_REACTIVATION_DELAY, + ); } - if (shouldRetry(error) === false) { + if (shouldRetry(error.cause) === false) { logger.warn({ msg: "Received JSON-RPC error", chain: chain.name, @@ -957,3 +950,13 @@ function shouldRetry(error: Error) { } return true; } + +function shouldRateLimit(error: Error) { + return ( + // @ts-ignore + error.code === 429 || + // @ts-ignore + error.status === 429 || + error instanceof TimeoutError + ); +} diff --git a/packages/core/src/runtime/historical.ts b/packages/core/src/runtime/historical.ts index 01b628825..f551cbc24 100644 --- a/packages/core/src/runtime/historical.ts +++ b/packages/core/src/runtime/historical.ts @@ -1,6 +1,6 @@ import type { Database } from "@/database/index.js"; import type { Common } from "@/internal/common.js"; -import { ShutdownError } from "@/internal/errors.js"; +// import { ShutdownError } from "@/internal/errors.js"; import type { Chain, CrashRecoveryCheckpoint, @@ -1286,8 +1286,8 @@ export async function* getLocalSyncGenerator(params: { params.common.options.command === "dev" ? 10_000 : 50_000, ); - closestToTipBlock = await params.database.syncQB - .transaction(async (tx) => { + closestToTipBlock = await params.database.syncQB.transaction( + async (tx) => { const syncStore = createSyncStore({ common: params.common, qb: tx }); const logs = await historicalSync.syncBlockRangeData({ interval, @@ -1328,22 +1328,25 @@ export async function* getLocalSyncGenerator(params: { } return closestToTipBlock; - }) - .catch((error) => { - if (error instanceof ShutdownError) { - throw error; - } - - params.common.logger.warn({ - msg: "Failed to fetch backfill JSON-RPC data", - chain: params.chain.name, - chain_id: params.chain.id, - block_range: JSON.stringify(interval), - duration: endClock(), - error, - }); - throw error; - }); + }, + ); + // TODO(kyle) don't log here because it's not a decision boundary, instead + // add context to inner errors. + // .catch((error) => { + // if (error instanceof ShutdownError) { + // throw error; + // } + + // params.common.logger.warn({ + // msg: "Failed to fetch backfill JSON-RPC data", + // chain: params.chain.name, + // chain_id: params.chain.id, + // block_range: JSON.stringify(interval), + // duration: endClock(), + // error, + // }); + // throw error; + // }); clearTimeout(durationTimer); diff --git a/packages/utils/src/getLogsRetryHelper.ts b/packages/utils/src/getLogsRetryHelper.ts index 28091608c..67dc48b93 100644 --- a/packages/utils/src/getLogsRetryHelper.ts +++ b/packages/utils/src/getLogsRetryHelper.ts @@ -2,13 +2,12 @@ import { type Address, type Hex, type LogTopic, - type RpcError, hexToBigInt, numberToHex, } from "viem"; export type GetLogsRetryHelperParameters = { - error: RpcError; + error: Error; params: [ { address?: Address | Address[]; @@ -109,6 +108,7 @@ export const getLogsRetryHelper = ({ // ankr match = sError.match("block range is too wide"); + // @ts-ignore if (match !== null && error.code === -32600) { const ranges = chunk({ params, range: 3000n }); From 5b1dca620b7a94a5cc0c45101e1469d85234ed04 Mon Sep 17 00:00:00 2001 From: Kyle Scott Date: Wed, 14 Jan 2026 10:56:28 -0500 Subject: [PATCH 05/11] more error handling --- packages/core/src/bin/commands/dev.ts | 53 ++- packages/core/src/bin/isolatedController.ts | 32 +- packages/core/src/bin/utils/exit.ts | 20 +- packages/core/src/build/index.ts | 6 +- packages/core/src/build/stacktrace.ts | 6 +- packages/core/src/database/index.ts | 54 +-- .../core/src/database/queryBuilder.test.ts | 3 +- packages/core/src/database/queryBuilder.ts | 156 ++++---- packages/core/src/drizzle/json.ts | 19 +- packages/core/src/indexing-store/cache.ts | 11 +- packages/core/src/indexing-store/index.ts | 38 +- packages/core/src/indexing-store/utils.ts | 47 +-- packages/core/src/indexing/index.ts | 90 +++-- packages/core/src/internal/errors.ts | 371 +++++++++--------- packages/core/src/internal/logger.ts | 29 +- packages/core/src/internal/types.ts | 12 +- packages/core/src/rpc/index.ts | 20 +- packages/core/src/runtime/isolated.ts | 47 +-- packages/core/src/runtime/multichain.ts | 48 +-- packages/core/src/runtime/omnichain.ts | 41 +- packages/core/src/server/error.ts | 4 +- packages/core/src/sync-historical/index.ts | 56 +-- 22 files changed, 549 insertions(+), 614 deletions(-) diff --git a/packages/core/src/bin/commands/dev.ts b/packages/core/src/bin/commands/dev.ts index 98449cf40..76e4f16e8 100644 --- a/packages/core/src/bin/commands/dev.ts +++ b/packages/core/src/bin/commands/dev.ts @@ -5,8 +5,8 @@ import { type Database, createDatabase } from "@/database/index.js"; import type { Common } from "@/internal/common.js"; import { BaseError, - NonRetryableUserError, ShutdownError, + isUserDerivedError, } from "@/internal/errors.js"; import { createLogger } from "@/internal/logger.js"; import { MetricsService } from "@/internal/metrics.js"; @@ -106,11 +106,11 @@ export async function dev({ cliOptions }: { cliOptions: CliOptions }) { } if (result.status === "error") { - if (isInitialBuild === false) { - common.logger.error({ - error: result.error, - }); - } + // if (isInitialBuild === false) { + // common.logger.error({ error: result.error }); + // } + + console.log(result.kind); // This handles indexing function build failures on hot reload. metrics.hasError = true; @@ -443,38 +443,31 @@ export async function dev({ cliOptions }: { cliOptions: CliOptions }) { process.on("uncaughtException", (error: Error) => { if (error instanceof ShutdownError) return; - if (error instanceof NonRetryableUserError) { - common.logger.error({ - msg: "uncaughtException", - error, - }); - - buildQueue.clear(); - buildQueue.add({ status: "error", kind: "indexing", error }); + if (error instanceof BaseError) { + common.logger.error({ msg: `uncaughtException: ${error.name}` }); + if (isUserDerivedError(error)) { + buildQueue.clear(); + buildQueue.add({ status: "error", kind: "indexing", error }); + } else { + exit({ code: 75 }); + } } else { - common.logger.error({ - msg: "uncaughtException", - error, - }); + common.logger.error({ msg: "uncaughtException", error }); exit({ code: 75 }); } }); process.on("unhandledRejection", (error: Error) => { if (error instanceof ShutdownError) return; if (error instanceof BaseError) { - common.logger.error({ - msg: `unhandledRejection: ${error.name}`, - }); - } else { - common.logger.error({ - msg: "unhandledRejection", - error, - }); - } - if (error instanceof NonRetryableUserError) { - buildQueue.clear(); - buildQueue.add({ status: "error", kind: "indexing", error }); + common.logger.error({ msg: `unhandledRejection: ${error.name}` }); + if (isUserDerivedError(error)) { + buildQueue.clear(); + buildQueue.add({ status: "error", kind: "indexing", error }); + } else { + exit({ code: 75 }); + } } else { + common.logger.error({ msg: "unhandledRejection", error }); exit({ code: 75 }); } }); diff --git a/packages/core/src/bin/isolatedController.ts b/packages/core/src/bin/isolatedController.ts index f744d1935..291108d55 100644 --- a/packages/core/src/bin/isolatedController.ts +++ b/packages/core/src/bin/isolatedController.ts @@ -5,10 +5,7 @@ import { Worker } from "node:worker_threads"; import { createIndexes, createViews } from "@/database/actions.js"; import { type Database, getPonderMetaTable } from "@/database/index.js"; import type { Common } from "@/internal/common.js"; -import { - NonRetryableUserError, - nonRetryableUserErrorNames, -} from "@/internal/errors.js"; +import {} from "@/internal/errors.js"; import { AggregateMetricsService, getAppProgress } from "@/internal/metrics.js"; import type { CrashRecoveryCheckpoint, @@ -248,14 +245,15 @@ export async function isolatedController({ break; } case "error": { - let error: Error; - if (nonRetryableUserErrorNames.includes(message.error.name)) { - error = new NonRetryableUserError(message.error.message); - } else { - error = new Error(message.error.message); - } - error.name = message.error.name; - error.stack = message.error.stack; + const error: Error = message.error; + // if (nonRetryableUserErrorNames.includes(message.error.name)) { + // error = new NonRetryableUserError(message.error.message); + // } else { + // error = new Error(message.error.message); + // } + // error = message.error; + // error.name = message.error.name; + // error.stack = message.error.stack; throw error; } } @@ -263,11 +261,11 @@ export async function isolatedController({ ); worker.on("error", (error: Error) => { - if (nonRetryableUserErrorNames.includes(error.name)) { - error = new NonRetryableUserError(error.message); - } else { - error = new Error(error.message); - } + // if (nonRetryableUserErrorNames.includes(error.name)) { + // error = new NonRetryableUserError(error.message); + // } else { + // error = new Error(error.message); + // } throw error; }); diff --git a/packages/core/src/bin/utils/exit.ts b/packages/core/src/bin/utils/exit.ts index 95d147ac2..10b81cb22 100644 --- a/packages/core/src/bin/utils/exit.ts +++ b/packages/core/src/bin/utils/exit.ts @@ -3,8 +3,8 @@ import readline from "node:readline"; import type { Common } from "@/internal/common.js"; import { BaseError, - NonRetryableUserError, ShutdownError, + isUserDerivedError, } from "@/internal/errors.js"; import type { Options } from "@/internal/options.js"; @@ -92,15 +92,16 @@ export const createExit = ({ common.logger.error({ msg: `unhandledRejection: ${error.name}`, }); + if (isUserDerivedError(error)) { + exit({ code: 1 }); + } else { + exit({ code: 75 }); + } } else { common.logger.error({ msg: "unhandledRejection", error, }); - } - if (error instanceof NonRetryableUserError) { - exit({ code: 1 }); - } else { exit({ code: 75 }); } }); @@ -110,15 +111,16 @@ export const createExit = ({ common.logger.error({ msg: `unhandledRejection: ${error.name}`, }); + if (isUserDerivedError(error)) { + exit({ code: 1 }); + } else { + exit({ code: 75 }); + } } else { common.logger.error({ msg: "unhandledRejection", error, }); - } - if (error instanceof NonRetryableUserError) { - exit({ code: 1 }); - } else { exit({ code: 75 }); } }); diff --git a/packages/core/src/build/index.ts b/packages/core/src/build/index.ts index 553f1a19d..ff08801b8 100644 --- a/packages/core/src/build/index.ts +++ b/packages/core/src/build/index.ts @@ -178,13 +178,13 @@ export const createBuild = async ({ return { status: "success", exports } as const; } catch (error_) { const relativePath = path.relative(common.options.rootDir, file); - // TODO(kyle) error should be a `BuildError` - const error = parseViteNodeError(relativePath, error_ as Error); + const error = new BuildError(undefined, { + cause: parseViteNodeError(relativePath, error_ as Error), + }); return { status: "error", error } as const; } }; - // TODO(kyle) all files should use this function const executeFileWithTimeout = async ({ file, }: { file: string }): Promise< diff --git a/packages/core/src/build/stacktrace.ts b/packages/core/src/build/stacktrace.ts index 9357d66bd..b8d43c22c 100644 --- a/packages/core/src/build/stacktrace.ts +++ b/packages/core/src/build/stacktrace.ts @@ -2,15 +2,15 @@ import { readFileSync } from "node:fs"; import { codeFrameColumns } from "@babel/code-frame"; import { parse as parseStackTrace } from "stacktrace-parser"; -class ESBuildTransformError extends Error { +export class ESBuildTransformError extends Error { override name = "ESBuildTransformError"; } -class ESBuildBuildError extends Error { +export class ESBuildBuildError extends Error { override name = "ESBuildBuildError"; } -class ESBuildContextError extends Error { +export class ESBuildContextError extends Error { override name = "ESBuildContextError"; } diff --git a/packages/core/src/database/index.ts b/packages/core/src/database/index.ts index 04d2dc593..553608247 100644 --- a/packages/core/src/database/index.ts +++ b/packages/core/src/database/index.ts @@ -8,7 +8,7 @@ import { import type { Common } from "@/internal/common.js"; import { MigrationError, - NonRetryableUserError, + QueryBuilderError, ShutdownError, } from "@/internal/errors.js"; import type { @@ -48,7 +48,7 @@ import { dropLiveQueryTriggers, dropTriggers, } from "./actions.js"; -import { type QB, createQB, parseQBError } from "./queryBuilder.js"; +import { type QB, createQB } from "./queryBuilder.js"; export type Database = { driver: PostgresDriver | PGliteDriver; @@ -454,7 +454,7 @@ export const createDatabase = ({ return; } catch (_error) { - const error = parseQBError(_error as Error); + const error = new QueryBuilderError({ cause: _error as Error }); if (common.shutdown.isKilled) { throw new ShutdownError(); @@ -475,14 +475,7 @@ export const createDatabase = ({ error, }); - if (error instanceof NonRetryableUserError) { - common.logger.warn({ - msg: "Failed database query", - query: "migrate_sync", - error, - }); - throw error; - } + // TODO(kyle) shouldRetry() if (i === 9) { common.logger.warn({ @@ -540,13 +533,10 @@ export const createDatabase = ({ ); } } catch (_error) { - let error = _error as Error; - if (!error.message.includes("already exists")) throw error; - error = new MigrationError( - `Unable to create table '${namespace.schema}'.'${schemaBuild.statements.tables.json[i]!.tableName}' because a table with that name already exists.`, + throw new MigrationError( + `Unable to create table '${namespace.schema}'.'${schemaBuild.statements.tables.json[i]!.tableName}'.`, + { cause: _error as Error }, ); - error.stack = undefined; - throw error; } } @@ -569,13 +559,10 @@ export const createDatabase = ({ context, ) .catch((_error) => { - const error = _error as Error; - if (!error.message.includes("already exists")) throw error; - const e = new MigrationError( - `Unable to create view "${namespace.schema}"."${schemaBuild.statements.views.json[i]!.name}" because a view with that name already exists.`, + throw new MigrationError( + `Unable to create view "${namespace.schema}"."${schemaBuild.statements.views.json[i]!.name}".`, + { cause: _error as Error }, ); - e.stack = undefined; - throw e; }); } }; @@ -588,13 +575,10 @@ export const createDatabase = ({ context, ) .catch((_error) => { - const error = _error as Error; - if (!error.message.includes("already exists")) throw error; - const e = new MigrationError( - `Unable to create enum "${namespace.schema}"."${schemaBuild.statements.enums.json[i]!.name}" because an enum with that name already exists.`, + throw new MigrationError( + `Unable to create enum "${namespace.schema}"."${schemaBuild.statements.enums.json[i]!.name}".`, + { cause: _error as Error }, ); - e.stack = undefined; - throw e; }); } }; @@ -854,11 +838,9 @@ CREATE TABLE IF NOT EXISTS "${namespace.schema}"."${PONDER_CHECKPOINT_TABLE_NAME // Note: ponder <=0.8 will evaluate this as true because the version is undefined if (previousApp.version !== VERSION) { - const error = new MigrationError( + throw new MigrationError( `Schema "${namespace.schema}" was previously used by a Ponder app with a different minor version. Drop the schema first, or use a different schema. Read more: https://ponder.sh/docs/database#database-schema`, ); - error.stack = undefined; - throw error; } if ( @@ -866,11 +848,9 @@ CREATE TABLE IF NOT EXISTS "${namespace.schema}"."${PONDER_CHECKPOINT_TABLE_NAME (common.options.command === "dev" || previousApp.build_id !== buildId) ) { - const error = new MigrationError( + throw new MigrationError( `Schema "${namespace.schema}" was previously used by a different Ponder app. Drop the schema first, or use a different schema. Read more: https://ponder.sh/docs/database#database-schema`, ); - error.stack = undefined; - throw error; } const expiry = @@ -986,11 +966,9 @@ CREATE TABLE IF NOT EXISTS "${namespace.schema}"."${PONDER_CHECKPOINT_TABLE_NAME result = await tryAcquireLockAndMigrate(); if (result.status === "locked") { - const error = new MigrationError( + throw new MigrationError( `Failed to acquire lock on schema "${namespace.schema}". A different Ponder app is actively using this schema.`, ); - error.stack = undefined; - throw error; } } diff --git a/packages/core/src/database/queryBuilder.test.ts b/packages/core/src/database/queryBuilder.test.ts index 460009874..a815b71cf 100644 --- a/packages/core/src/database/queryBuilder.test.ts +++ b/packages/core/src/database/queryBuilder.test.ts @@ -1,5 +1,4 @@ import { context, setupCommon, setupIsolatedDatabase } from "@/_test/setup.js"; -import { NotNullConstraintError } from "@/internal/errors.js"; import { createPool } from "@/utils/pg.js"; import { sql } from "drizzle-orm"; import { drizzle } from "drizzle-orm/node-postgres"; @@ -111,7 +110,7 @@ test("QB transaction retries error", async () => { connection.release(); }); -test("QB parses error", async () => { +test.skip("QB parses error", async () => { if (context.databaseConfig.kind !== "postgres") return; const pool = createPool( diff --git a/packages/core/src/database/queryBuilder.ts b/packages/core/src/database/queryBuilder.ts index a80bacf74..64e7cfdd8 100644 --- a/packages/core/src/database/queryBuilder.ts +++ b/packages/core/src/database/queryBuilder.ts @@ -1,20 +1,11 @@ import crypto from "node:crypto"; import type { Common } from "@/internal/common.js"; import { - BaseError, + QueryBuilderError, + ShutdownError, TransactionCallbackError, - TransactionControlError, TransactionStatementError, } from "@/internal/errors.js"; -import { - BigIntSerializationError, - CheckConstraintError, - DbConnectionError, - NonRetryableUserError, - NotNullConstraintError, - ShutdownError, - UniqueConstraintError, -} from "@/internal/errors.js"; import type { Logger } from "@/internal/logger.js"; import type { Schema } from "@/internal/types.js"; import type { Drizzle } from "@/types/db.js"; @@ -94,51 +85,51 @@ export type QB< | { $dialect: "postgres"; $client: pg.Pool | pg.PoolClient } ); -export const parseQBError = (error: Error): Error => { - // TODO(kyle) how to know if the error is a query builder error? - - // TODO(kyle) do we need this? - if (error instanceof BaseError) return error; - - if (error?.message?.includes("violates not-null constraint")) { - return new NotNullConstraintError(undefined, { cause: error }); - } else if (error?.message?.includes("violates unique constraint")) { - return new UniqueConstraintError(undefined, { cause: error }); - } else if (error?.message?.includes("violates check constraint")) { - return new CheckConstraintError(undefined, { cause: error }); - } else if ( - // nodejs error message - error?.message?.includes("Do not know how to serialize a BigInt") || - // bun error message - error?.message?.includes("cannot serialize BigInt") - ) { - const bigIntSerializationError = new BigIntSerializationError(undefined, { - cause: error, - }); - bigIntSerializationError.meta.push( - "Hint:\n The JSON column type does not support BigInt values. Use the replaceBigInts() helper function before inserting into the database. Docs: https://ponder.sh/docs/api-reference/ponder-utils#replacebigints", - ); - return bigIntSerializationError; - } else if (error?.message?.includes("does not exist")) { - return new NonRetryableUserError(error.message); - } else if (error?.message?.includes("already exists")) { - return new NonRetryableUserError(error.message); - } else if ( - error?.message?.includes( - "terminating connection due to administrator command", - ) || - error?.message?.includes("connection to client lost") || - error?.message?.includes("too many clients already") || - error?.message?.includes("Connection terminated unexpectedly") || - error?.message?.includes("ECONNRESET") || - error?.message?.includes("ETIMEDOUT") || - error?.message?.includes("timeout exceeded when trying to connect") - ) { - return new DbConnectionError(error.message); - } - - return error; -}; +// export const parseQBError = (error: Error): Error => { +// // TODO(kyle) how to know if the error is a query builder error? + +// // TODO(kyle) do we need this? +// if (error instanceof BaseError) return error; + +// if (error?.message?.includes("violates not-null constraint")) { +// return new NotNullConstraintError(undefined, { cause: error }); +// } else if (error?.message?.includes("violates unique constraint")) { +// return new UniqueConstraintError(undefined, { cause: error }); +// } else if (error?.message?.includes("violates check constraint")) { +// return new CheckConstraintError(undefined, { cause: error }); +// } else if ( +// // nodejs error message +// error?.message?.includes("Do not know how to serialize a BigInt") || +// // bun error message +// error?.message?.includes("cannot serialize BigInt") +// ) { +// const bigIntSerializationError = new BigIntSerializationError(undefined, { +// cause: error, +// }); +// bigIntSerializationError.meta.push( +// "Hint:\n The JSON column type does not support BigInt values. Use the replaceBigInts() helper function before inserting into the database. Docs: https://ponder.sh/docs/api-reference/ponder-utils#replacebigints", +// ); +// return bigIntSerializationError; +// } else if (error?.message?.includes("does not exist")) { +// return new NonRetryableUserError(error.message); +// } else if (error?.message?.includes("already exists")) { +// return new NonRetryableUserError(error.message); +// } else if ( +// error?.message?.includes( +// "terminating connection due to administrator command", +// ) || +// error?.message?.includes("connection to client lost") || +// error?.message?.includes("too many clients already") || +// error?.message?.includes("Connection terminated unexpectedly") || +// error?.message?.includes("ECONNRESET") || +// error?.message?.includes("ETIMEDOUT") || +// error?.message?.includes("timeout exceeded when trying to connect") +// ) { +// return new DbConnectionError(error.message); +// } + +// return error; +// }; /** * Create a query builder. @@ -217,7 +208,15 @@ export const createQB = < return result; } catch (_error) { - let error = parseQBError(_error as Error); + const error = new QueryBuilderError({ cause: _error as Error }); + + // TODO(kyle) determine transaction control error? + // if ( + // isTransaction && + // error instanceof TransactionStatementError === false && + // error instanceof TransactionCallbackError === false + // ) { + // } if (common.shutdown.isKilled) { throw new ShutdownError(); @@ -238,12 +237,12 @@ export const createQB = < firstError = error; } - // Three types of query environments - // 1. Query outside of a transaction: Retry immediately. - // 2. Query inside of a transaction: Throw error, retry later. - // We want the error bubbled up out of the transaction callback scope, so the - // so the control flow can rollback the transaction. - // 3. Transaction callback: Retry immediately if the error was from #2 or from control statements, else throw error. + // Contexts: + // 1. Query outside of a transaction. + // 2. Query inside of a transaction. + // 3. Transaction query: Could be caused by a query in the transaction callback, + // a transaction control statement, or a wildcard error that is not related to + // the query builder. if (isTransaction === false && isTransactionStatement) { logger.warn({ @@ -253,28 +252,15 @@ export const createQB = < duration: endClock(), error, }); - // Transaction statements are not immediately retried, so the transaction will be properly rolled back. - throw new TransactionStatementError(undefined, { cause: error }); - } else if (error instanceof TransactionCallbackError) { - throw error.cause; - } else if (error instanceof NonRetryableUserError) { - logger.warn({ - msg: "Failed database query", - query: label, - query_id: id, - duration: endClock(), - error, - }); - throw error; + // Transaction statements are not immediately retried, so the transaction + // will be properly rolled back. + throw new TransactionStatementError({ cause: error }); + } else if (error.cause instanceof TransactionCallbackError) { + // Unrelated errors are bubbled out of the query builder. + throw error.cause.cause; } - if ( - isTransaction && - error instanceof TransactionStatementError === false && - error instanceof TransactionCallbackError === false - ) { - error = new TransactionControlError(undefined, { cause: error }); - } + // TODO(kyle) shouldRetry() if (i === RETRY_COUNT) { logger.warn({ @@ -596,3 +582,7 @@ export const createQB = < return qb; }; + +// function shouldRetry(error: Error) { +// return true; +// } diff --git a/packages/core/src/drizzle/json.ts b/packages/core/src/drizzle/json.ts index a1c61c72f..e8b952077 100644 --- a/packages/core/src/drizzle/json.ts +++ b/packages/core/src/drizzle/json.ts @@ -1,4 +1,4 @@ -import { type BaseError, BigIntSerializationError } from "@/internal/errors.js"; +import { BigIntSerializationError } from "@/internal/errors.js"; import { type ColumnBaseConfig, entityKind } from "drizzle-orm"; import type { ColumnBuilderBaseConfig, @@ -61,8 +61,8 @@ export class PgJson< // bun error message error?.message?.includes("cannot serialize BigInt") ) { - error = new BigIntSerializationError(error.message); - (error as BaseError).meta.push( + error = new BigIntSerializationError({ cause: error }); + (error as BigIntSerializationError).meta.push( "Hint:\n The JSON column type does not support BigInt values. Use the replaceBigInts() helper function before inserting into the database. Docs: https://ponder.sh/docs/api-reference/ponder-utils#replacebigints", ); } @@ -135,10 +135,15 @@ export class PgJsonb< return JSON.stringify(value); } catch (_error) { let error = _error as Error; - if (error?.message?.includes("Do not know how to serialize a BigInt")) { - error = new BigIntSerializationError(error.message); - (error as BaseError).meta.push( - "Hint:\n The JSON column type does not support BigInt values. Use the replaceBigInts() helper function before inserting into the database. Docs: https://ponder.sh/docs/api-reference/ponder-utils#replacebigints", + if ( + // node error message + error?.message?.includes("Do not know how to serialize a BigInt") || + // bun error message + error?.message?.includes("cannot serialize BigInt") + ) { + error = new BigIntSerializationError({ cause: error }); + (error as BigIntSerializationError).meta.push( + "Hint:\n The JSONB column type does not support BigInt values. Use the replaceBigInts() helper function before inserting into the database. Docs: https://ponder.sh/docs/api-reference/ponder-utils#replacebigints", ); } diff --git a/packages/core/src/indexing-store/cache.ts b/packages/core/src/indexing-store/cache.ts index 5ed6b231d..8df765e45 100644 --- a/packages/core/src/indexing-store/cache.ts +++ b/packages/core/src/indexing-store/cache.ts @@ -286,7 +286,7 @@ export const getCopyHelper = (qb: QB, chainId?: number) => { blob: new Blob([text]), }) .catch((error) => { - throw new CopyFlushError(undefined, { cause: error as Error }); + throw new CopyFlushError({ cause: error as Error }); }); }; } else { @@ -305,7 +305,7 @@ export const getCopyHelper = (qb: QB, chainId?: number) => { copyStream.write(text); copyStream.end(); }).catch((error) => { - throw new CopyFlushError(undefined, { cause: error as Error }); + throw new CopyFlushError({ cause: error as Error }); }); }; } @@ -755,8 +755,7 @@ export const createIndexingCache = ({ ); if (result.status === "error") { - error = new DelayedInsertError(result.error.message); - error.stack = undefined; + error = new DelayedInsertError({ cause: result.error as Error }); addErrorMeta( error, @@ -827,9 +826,7 @@ export const createIndexingCache = ({ ); if (result.status === "error") { - error = new DelayedInsertError(undefined, { - cause: result.error as Error, - }); + error = new DelayedInsertError({ cause: result.error as Error }); addErrorMeta( error, diff --git a/packages/core/src/indexing-store/index.ts b/packages/core/src/indexing-store/index.ts index fc2bd1b1c..408ad0d28 100644 --- a/packages/core/src/indexing-store/index.ts +++ b/packages/core/src/indexing-store/index.ts @@ -1,16 +1,7 @@ import type { QB } from "@/database/queryBuilder.js"; import { onchain } from "@/drizzle/onchain.js"; import type { Common } from "@/internal/common.js"; -import { - InvalidStoreAccessError, - InvalidStoreMethodError, - NonRetryableUserError, - RawSqlError, - RecordNotFoundError, - RetryableError, - UndefinedTableError, - UniqueConstraintError, -} from "@/internal/errors.js"; +import { IndexingDBError, RawSqlError } from "@/internal/errors.js"; import type { Schema } from "@/internal/types.js"; import type { IndexingErrorHandler, SchemaBuild } from "@/internal/types.js"; import type { Db } from "@/types/db.js"; @@ -52,7 +43,7 @@ export const validateUpdateSet = ( if (js in set) { // Note: Noop on the primary keys if they are identical, otherwise throw an error. if ((set as Row)[js] !== prev[js]) { - throw new NonRetryableUserError( + throw new IndexingDBError( `Primary key column '${js}' cannot be updated`, ); } @@ -66,13 +57,13 @@ export const checkOnchainTable = ( method: "find" | "insert" | "update" | "delete", ) => { if (table === undefined) - throw new UndefinedTableError( + throw new IndexingDBError( `Table object passed to db.${method}() is undefined`, ); if (onchain in table) return; - throw new InvalidStoreMethodError( + throw new IndexingDBError( method === "find" ? `db.find() can only be used with onchain tables, and '${getTableConfig(table).name}' is an offchain table or a view.` : `Indexing functions can only write to onchain tables, and '${getTableConfig(table).name}' is an offchain table or a view.`, @@ -87,7 +78,7 @@ export const checkTableAccess = ( ) => { if (chainId === undefined) return; if ("chainId" in key && String(key.chainId) === String(chainId)) return; - throw new InvalidStoreAccessError( + throw new IndexingDBError( "chainId" in key ? `db.${method}(${getTableConfig(table).name}) cannot access rows on different chains when ordering is 'isolated'.` : `db.${method}(${getTableConfig(table).name}) must specify 'chainId' when ordering is 'isolated'.`, @@ -120,7 +111,7 @@ export const createIndexingStore = ({ return async (...args: any[]) => { try { if (isProcessingEvents === false) { - throw new NonRetryableUserError( + throw new IndexingDBError( "A store API method (find, update, insert, delete) was called after the indexing function returned. Hint: Did you forget to await the store API method call (an unawaited promise)?", ); } @@ -128,20 +119,21 @@ export const createIndexingStore = ({ const result = await fn(...args); // @ts-expect-error typescript bug lol if (isProcessingEvents === false) { - throw new NonRetryableUserError( + throw new IndexingDBError( "A store API method (find, update, insert, delete) was called after the indexing function returned. Hint: Did you forget to await the store API method call (an unawaited promise)?", ); } return result; } catch (error) { if (isProcessingEvents === false) { - throw new NonRetryableUserError( + throw new IndexingDBError( "A store API method (find, update, insert, delete) was called after the indexing function returned. Hint: Did you forget to await the store API method call (an unawaited promise)?", ); } - if (error instanceof RetryableError) { - indexingErrorHandler.setRetryableError(error); + // Note: `error` must be an internal ponder error rather than a logical user error. + if (error instanceof IndexingDBError === false) { + indexingErrorHandler.setError(error as Error); } throw error; @@ -384,7 +376,7 @@ export const createIndexingStore = ({ }); if (row) { - throw new UniqueConstraintError( + throw new IndexingDBError( `Primary key conflict in table '${getTableName(table)}'`, ); } else { @@ -429,7 +421,7 @@ export const createIndexingStore = ({ }); if (row) { - throw new UniqueConstraintError( + throw new IndexingDBError( `Primary key conflict in table '${getTableName(table)}'`, ); } else { @@ -493,7 +485,7 @@ export const createIndexingStore = ({ const ponderRowUpdate = await indexingCache.get({ table, key }); if (ponderRowUpdate === null) { - const error = new RecordNotFoundError( + const error = new IndexingDBError( `No existing record found in table '${getTableName(table)}'`, ); error.meta.push(`db.update arguments:\n${prettyPrint(key)}`); @@ -603,7 +595,7 @@ export const createIndexingStore = ({ return result; }); } catch (error) { - throw new RawSqlError(undefined, { cause: error as Error }); + throw new RawSqlError({ cause: error as Error }); } finally { common.metrics.ponder_indexing_store_raw_sql_duration.observe( endClock(), diff --git a/packages/core/src/indexing-store/utils.ts b/packages/core/src/indexing-store/utils.ts index 6104944a5..8e6249a20 100644 --- a/packages/core/src/indexing-store/utils.ts +++ b/packages/core/src/indexing-store/utils.ts @@ -1,8 +1,5 @@ import { getPrimaryKeyColumns } from "@/drizzle/index.js"; -import { - BigIntSerializationError, - NotNullConstraintError, -} from "@/internal/errors.js"; +import { IndexingDBError } from "@/internal/errors.js"; import { prettyPrint } from "@/utils/print.js"; import { type Column, @@ -64,37 +61,23 @@ export const normalizeColumn = ( if (value === null) return null; if (column.mapToDriverValue === undefined) return value; - try { - if (Array.isArray(value) && column instanceof PgArray) { - return value.map((v) => { - if (column.baseColumn.columnType === "PgTimestamp") { - return v; - } - - return column.baseColumn.mapFromDriverValue( - column.baseColumn.mapToDriverValue(v), - ); - }); - } + if (Array.isArray(value) && column instanceof PgArray) { + return value.map((v) => { + if (column.baseColumn.columnType === "PgTimestamp") { + return v; + } - if (column.columnType === "PgTimestamp") { - return value; - } - - return column.mapFromDriverValue(column.mapToDriverValue(value)); - } catch (e) { - if ( - (e as Error)?.message?.includes("Do not know how to serialize a BigInt") - ) { - const error = new BigIntSerializationError((e as Error).message); - error.meta.push( - "Hint:\n The JSON column type does not support BigInt values. Use the replaceBigInts() helper function before inserting into the database. Docs: https://ponder.sh/docs/api-reference/ponder-utils#replacebigints", + return column.baseColumn.mapFromDriverValue( + column.baseColumn.mapToDriverValue(v), ); - throw error; - } + }); + } - throw e; + if (column.columnType === "PgTimestamp") { + return value; } + + return column.mapFromDriverValue(column.mapToDriverValue(value)); }; export const normalizeRow = ( @@ -110,7 +93,7 @@ export const normalizeRow = ( column.notNull && hasEmptyValue(column) === false ) { - const error = new NotNullConstraintError( + const error = new IndexingDBError( `Column '${getTableName( table, )}.${columnName}' violates not-null constraint.`, diff --git a/packages/core/src/indexing/index.ts b/packages/core/src/indexing/index.ts index ef5051310..9e8b025f2 100644 --- a/packages/core/src/indexing/index.ts +++ b/packages/core/src/indexing/index.ts @@ -211,13 +211,13 @@ export const createIndexing = ({ await event.setupCallback.fn(indexingFunctionArg); - // Note: Check `getRetryableError` to handle user-code catching errors + // Note: Check `getError` to handle user-code catching errors // from the indexing store. - if (indexingErrorHandler.getRetryableError()) { - const retryableError = indexingErrorHandler.getRetryableError()!; - indexingErrorHandler.clearRetryableError(); - throw retryableError; + if (indexingErrorHandler.getError()) { + const error = indexingErrorHandler.getError()!; + indexingErrorHandler.clearError(); + throw error; } common.metrics.ponder_indexing_function_duration.observe( @@ -225,21 +225,25 @@ export const createIndexing = ({ endClock(), ); } catch (_error) { - let error = _error instanceof Error ? _error : new Error(String(_error)); + if (common.shutdown.isKilled) { + throw new ShutdownError(); + } - // Note: Use `getRetryableError` rather than `error` to avoid + // Note: Use `getError` rather than `error` to avoid // issues with the user-code augmenting errors from the indexing store. - if (indexingErrorHandler.getRetryableError()) { - const retryableError = indexingErrorHandler.getRetryableError()!; - indexingErrorHandler.clearRetryableError(); - error = retryableError; - } - - if (common.shutdown.isKilled) { - throw new ShutdownError(); + let error: IndexingFunctionError; + if (indexingErrorHandler.getError()) { + error = new IndexingFunctionError({ + cause: indexingErrorHandler.getError()!, + }); + indexingErrorHandler.clearError(); + } else { + error = new IndexingFunctionError({ cause: _error as Error }); } + // Copy the stack from the inner error. + error.stack = error.cause.stack; addStackTrace(error, common.options); addErrorMeta(error, toErrorMeta(event)); @@ -255,7 +259,7 @@ export const createIndexing = ({ common.metrics.hasError = true; - throw new IndexingFunctionError(undefined, { cause: error as Error }); + throw new IndexingFunctionError({ cause: error as Error }); } }; @@ -278,39 +282,43 @@ export const createIndexing = ({ await event.eventCallback.fn(indexingFunctionArg); + // Note: Check `getError` to handle user-code catching errors + // from the indexing store. + + if (indexingErrorHandler.getError()) { + const error = indexingErrorHandler.getError()!; + indexingErrorHandler.clearError(); + throw error; + } + common.metrics.ponder_indexing_function_duration.observe( metricLabels[event.eventCallback.name]!, endClock(), ); - - // Note: Check `getRetryableError` to handle user-code catching errors - // from the indexing store. - - if (indexingErrorHandler.getRetryableError()) { - const retryableError = indexingErrorHandler.getRetryableError()!; - indexingErrorHandler.clearRetryableError(); - throw retryableError; - } } catch (_error) { - let error = _error instanceof Error ? _error : new Error(String(_error)); + if (common.shutdown.isKilled) { + throw new ShutdownError(); + } - // Note: Use `getRetryableError` rather than `error` to avoid + // Note: Use `getError` rather than `error` to avoid // issues with the user-code augmenting errors from the indexing store. - if (indexingErrorHandler.getRetryableError()) { - const retryableError = indexingErrorHandler.getRetryableError()!; - indexingErrorHandler.clearRetryableError(); - error = retryableError; - } - - if (common.shutdown.isKilled) { - throw new ShutdownError(); + let error: IndexingFunctionError; + if (indexingErrorHandler.getError()) { + error = new IndexingFunctionError({ + cause: indexingErrorHandler.getError()!, + }); + indexingErrorHandler.clearError(); + } else { + error = new IndexingFunctionError({ cause: _error as Error }); } - if (error instanceof InvalidEventAccessError) { - throw error; + if (error.cause instanceof InvalidEventAccessError) { + throw error.cause; } + // Copy the stack from the inner error. + error.stack = error.cause.stack; addStackTrace(error, common.options); addErrorMeta(error, toErrorMeta(event)); @@ -327,7 +335,7 @@ export const createIndexing = ({ common.metrics.hasError = true; - throw new IndexingFunctionError(undefined, { cause: error as Error }); + throw error; } }; @@ -776,7 +784,7 @@ export const createEventProxy = < resetFilterInclude(eventName); // @ts-expect-error const error = new InvalidEventAccessError(`${type}.${prop}`); - indexingErrorHandler.setRetryableError(error); + indexingErrorHandler.setError(error); throw error; } @@ -808,7 +816,7 @@ export const createEventProxy = < resetFilterInclude(eventName); // @ts-expect-error const error = new InvalidEventAccessError(`${type}.${prop}`); - indexingErrorHandler.setRetryableError(error); + indexingErrorHandler.setError(error); throw error; } @@ -833,7 +841,7 @@ export const createEventProxy = < resetFilterInclude(eventName); // @ts-expect-error const error = new InvalidEventAccessError(`${type}.${prop}`); - indexingErrorHandler.setRetryableError(error); + indexingErrorHandler.setError(error); throw error; } diff --git a/packages/core/src/internal/errors.ts b/packages/core/src/internal/errors.ts index f247fd136..9528bc856 100644 --- a/packages/core/src/internal/errors.ts +++ b/packages/core/src/internal/errors.ts @@ -1,283 +1,286 @@ +import { + ESBuildBuildError, + ESBuildContextError, + ESBuildTransformError, +} from "@/build/stacktrace.js"; +import type { getLogsRetryHelper } from "@ponder/utils"; + /** Base class for all known errors. */ -export class BaseError extends Error { +export class BaseError< + cause extends Error | undefined = undefined, +> extends Error { override name = "BaseError"; + override cause: cause; meta: string[] = []; - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + constructor(message?: string | undefined, { cause }: { cause?: cause } = {}) { super(message, { cause }); + this.cause = cause as cause; Object.setPrototypeOf(this, BaseError.prototype); } } -/** Error caused by user code. Should not be retried. */ -export class NonRetryableUserError extends BaseError { - override name = "NonRetryableUserError"; - - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { - super(message, { cause }); - Object.setPrototypeOf(this, NonRetryableUserError.prototype); - } -} - -/** Error that may succeed if tried again. */ -export class RetryableError extends BaseError { - override name = "RetryableError"; - - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { - super(message, { cause }); - Object.setPrototypeOf(this, RetryableError.prototype); - } -} - -export class ShutdownError extends NonRetryableUserError { +export class ShutdownError extends BaseError { override name = "ShutdownError"; - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { - super(message, { cause }); + constructor(message?: string | undefined) { + super(message); Object.setPrototypeOf(this, ShutdownError.prototype); } } -export class BuildError extends NonRetryableUserError { +export class BuildError< + cause extends Error | undefined = undefined, +> extends BaseError { override name = "BuildError"; - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + constructor(message?: string | undefined, { cause }: { cause?: cause } = {}) { super(message, { cause }); Object.setPrototypeOf(this, BuildError.prototype); } } -export class MigrationError extends NonRetryableUserError { - override name = "MigrationError"; - - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { - super(message, { cause }); - Object.setPrototypeOf(this, MigrationError.prototype); - } -} - -// Non-retryable database errors - -export class UniqueConstraintError extends NonRetryableUserError { - override name = "UniqueConstraintError"; - - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { - super(message, { cause }); - Object.setPrototypeOf(this, UniqueConstraintError.prototype); - } -} - -export class NotNullConstraintError extends NonRetryableUserError { - override name = "NotNullConstraintError"; +export class RpcRequestError< + cause extends Error | undefined = undefined, +> extends BaseError { + override name = "RpcRequestError"; - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + constructor(message?: string | undefined, { cause }: { cause?: cause } = {}) { super(message, { cause }); - Object.setPrototypeOf(this, NotNullConstraintError.prototype); + Object.setPrototypeOf(this, RpcRequestError.prototype); } } -export class InvalidStoreAccessError extends NonRetryableUserError { - override name = "InvalidStoreAccessError"; - - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { - super(message, { cause }); - Object.setPrototypeOf(this, InvalidStoreAccessError.prototype); +export class EthGetLogsRangeError extends BaseError< + RpcRequestError +> { + override name = "EthGetLogsRangeError"; + override cause: RpcRequestError; + isSuggestedRange: Extract< + ReturnType, + { shouldRetry: true } + >["isSuggestedRange"]; + ranges: Extract< + ReturnType, + { shouldRetry: true } + >["ranges"]; + + constructor( + { cause }: { cause: RpcRequestError }, + params: Extract< + ReturnType, + { shouldRetry: true } + >, + ) { + super(undefined, { cause }); + this.cause = cause; + this.isSuggestedRange = params.isSuggestedRange; + this.ranges = params.ranges; + Object.setPrototypeOf(this, EthGetLogsRangeError.prototype); } } -export class RecordNotFoundError extends NonRetryableUserError { - override name = "RecordNotFoundError"; +export class QueryBuilderError< + cause extends Error | undefined = undefined, +> extends BaseError { + override name = "QueryBuilderError"; - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { - super(message, { cause }); - Object.setPrototypeOf(this, RecordNotFoundError.prototype); + constructor({ cause }: { cause?: cause } = {}) { + super(undefined, { cause }); + Object.setPrototypeOf(this, QueryBuilderError.prototype); } } -export class CheckConstraintError extends NonRetryableUserError { - override name = "CheckConstraintError"; +/** + * Error caused by an individual `qb.wrap` statement inside + * of a `qb.transaction` callback. + */ +export class TransactionStatementError< + cause extends Error | undefined = undefined, +> extends BaseError { + override name = "TransactionStatementError"; - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { - super(message, { cause }); - Object.setPrototypeOf(this, CheckConstraintError.prototype); + constructor({ cause }: { cause?: cause } = {}) { + super(undefined, { cause }); + Object.setPrototypeOf(this, TransactionStatementError.prototype); } } -// Retryable database errors - -export class DbConnectionError extends RetryableError { - override name = "DbConnectionError"; +/** + * Error thrown from a `qb.transaction` callback not caused by a `qb.wrap` statement. + */ +export class TransactionCallbackError< + cause extends Error, +> extends BaseError { + override name = "TransactionCallbackError"; - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { - super(message, { cause }); - Object.setPrototypeOf(this, DbConnectionError.prototype); + constructor({ cause }: { cause: cause }) { + super(undefined, { cause }); + Object.setPrototypeOf(this, TransactionCallbackError.prototype); } } -// export class TransactionStatementError extends RetryableError { -// override name = "TransactionStatementError"; - -// constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { -// super(message, { cause }); -// Object.setPrototypeOf(this, TransactionStatementError.prototype); -// } -// } - -export class CopyFlushError extends RetryableError { +export class CopyFlushError< + cause extends Error | undefined = undefined, +> extends BaseError { override name = "CopyFlushError"; - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { - super(message, { cause }); + constructor({ cause }: { cause?: cause } = {}) { + super(undefined, { cause }); Object.setPrototypeOf(this, CopyFlushError.prototype); } } -export class InvalidEventAccessError extends RetryableError { - override name = "InvalidEventAccessError"; - key: string; - - constructor(key: string, message?: string | undefined) { - super(message); - Object.setPrototypeOf(this, InvalidEventAccessError.prototype); +export class DelayedInsertError< + cause extends Error | undefined = undefined, +> extends BaseError { + override name = "DelayedInsertError"; - this.key = key; + constructor({ cause }: { cause?: cause } = {}) { + super(undefined, { cause }); + Object.setPrototypeOf(this, DelayedInsertError.prototype); } } -// Non-retryable indexing store errors - -export class InvalidStoreMethodError extends NonRetryableUserError { - override name = "InvalidStoreMethodError"; +export class IndexingDBError< + cause extends Error | undefined = undefined, +> extends BaseError { + override name = "IndexingDBError"; - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + constructor(message?: string | undefined, { cause }: { cause?: cause } = {}) { super(message, { cause }); - Object.setPrototypeOf(this, InvalidStoreMethodError.prototype); + Object.setPrototypeOf(this, IndexingDBError.prototype); } } -export class UndefinedTableError extends NonRetryableUserError { - override name = "UndefinedTableError"; +export class RawSqlError< + cause extends Error | undefined = undefined, +> extends BaseError { + override name = "RawSqlError"; - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { - super(message, { cause }); - Object.setPrototypeOf(this, UndefinedTableError.prototype); + constructor({ cause }: { cause?: cause } = {}) { + super(undefined, { cause }); + Object.setPrototypeOf(this, RawSqlError.prototype); } } -export class BigIntSerializationError extends NonRetryableUserError { +export class BigIntSerializationError< + cause extends Error | undefined = undefined, +> extends BaseError { override name = "BigIntSerializationError"; - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { - super(message, { cause }); + constructor({ cause }: { cause?: cause } = {}) { + super(undefined, { cause }); Object.setPrototypeOf(this, BigIntSerializationError.prototype); } } -export class DelayedInsertError extends NonRetryableUserError { - override name = "DelayedInsertError"; - - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { - super(message, { cause }); - Object.setPrototypeOf(this, DelayedInsertError.prototype); - } -} - -export class RawSqlError extends NonRetryableUserError { - override name = "RawSqlError"; +/** + * @dev `stack` property points to the user code that caused the error. + */ +export class ServerError< + cause extends Error | undefined = undefined, +> extends BaseError { + override name = "ServerError"; - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { - super(message, { cause }); - Object.setPrototypeOf(this, RawSqlError.prototype); + constructor({ cause }: { cause?: cause } = {}) { + super(undefined, { cause }); + Object.setPrototypeOf(this, ServerError.prototype); } } -export class IndexingFunctionError extends NonRetryableUserError { +/** + * @dev `stack` property points to the user code that caused the error. + */ +export class IndexingFunctionError< + cause extends Error | undefined = undefined, +> extends BaseError { override name = "IndexingFunctionError"; - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { - super(message, { cause }); + constructor({ cause }: { cause?: cause } = {}) { + super(undefined, { cause }); Object.setPrototypeOf(this, IndexingFunctionError.prototype); } } /** - * @dev All JSON-RPC request errors are retryable. + * Error throw when an `event` property is unexpectedly accessed in an indexing function. */ -export class RpcRequestError< - cause extends Error | undefined = undefined, -> extends RetryableError { - override name = "RpcRequestError"; - override cause: cause; +export class InvalidEventAccessError extends BaseError { + override name = "InvalidEventAccessError"; + key: string; - constructor(message?: string | undefined, { cause }: { cause?: cause } = {}) { - super(message, { cause }); - // @ts-ignore - this.cause = cause; - Object.setPrototypeOf(this, RpcRequestError.prototype); + constructor(key: string) { + super(); + this.key = key; + Object.setPrototypeOf(this, InvalidEventAccessError.prototype); } } -export class QueryBuilderRetryableError extends RetryableError { - override name = "QueryBuilderRetryableError"; +export class MigrationError< + cause extends Error | undefined = undefined, +> extends BaseError { + override name = "MigrationError"; + // TODO(kyle) exit code - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { + constructor(message?: string | undefined, { cause }: { cause?: cause } = {}) { super(message, { cause }); - Object.setPrototypeOf(this, QueryBuilderRetryableError.prototype); + Object.setPrototypeOf(this, MigrationError.prototype); } } -export class QueryBuilderNonRetryableError extends NonRetryableUserError { - override name = "QueryBuilderNonRetryableError"; +// export const nonRetryableUserErrorNames = [ +// ShutdownError, +// BuildError, +// MigrationError, +// UniqueConstraintError, +// NotNullConstraintError, +// InvalidStoreAccessError, +// RecordNotFoundError, +// CheckConstraintError, +// InvalidStoreMethodError, +// UndefinedTableError, +// BigIntSerializationError, +// DelayedInsertError, +// RawSqlError, +// IndexingFunctionError, +// ].map((err) => err.name); - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { - super(message, { cause }); - Object.setPrototypeOf(this, QueryBuilderNonRetryableError.prototype); +/** + * Returns true if the error is derived from a logical error in user code. + */ +export function isUserDerivedError(error: BaseError): boolean { + if (error instanceof BuildError) return true; + if (error instanceof IndexingDBError) return true; + if (error instanceof DelayedInsertError) return true; + if ( + error instanceof IndexingFunctionError && + error.cause instanceof BaseError === false + ) { + return true; } -} -export class TransactionControlError extends QueryBuilderRetryableError { - override name = "TransactionControlError"; - - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { - super(message, { cause }); - Object.setPrototypeOf(this, TransactionControlError.prototype); + if (error instanceof BaseError && error.cause) { + if (isUserDerivedError(error.cause)) return true; } + return false; } -export class TransactionStatementError extends QueryBuilderRetryableError { - override name = "TransactionStatementError"; - - constructor(message?: string | undefined, { cause }: { cause?: Error } = {}) { - super(message, { cause }); - Object.setPrototypeOf(this, TransactionStatementError.prototype); +export function stripErrorStack(error: Error): void { + if (shouldPrintErrorStack(error) === false) { + error.stack = undefined; } -} - -export class TransactionCallbackError extends QueryBuilderRetryableError { - override name = "TransactionCallbackError"; - override cause: Error; - - constructor({ cause }: { cause: Error }) { - super(undefined, { cause }); - this.cause = cause; - Object.setPrototypeOf(this, TransactionCallbackError.prototype); + if (error instanceof BaseError && error.cause) { + stripErrorStack(error.cause); } } -export const nonRetryableUserErrorNames = [ - ShutdownError, - BuildError, - MigrationError, - UniqueConstraintError, - NotNullConstraintError, - InvalidStoreAccessError, - RecordNotFoundError, - CheckConstraintError, - InvalidStoreMethodError, - UndefinedTableError, - BigIntSerializationError, - DelayedInsertError, - RawSqlError, - IndexingFunctionError, -].map((err) => err.name); +export function shouldPrintErrorStack(error: Error): boolean { + if (error instanceof ServerError) return true; + if (error instanceof IndexingFunctionError) return true; + if (error instanceof ESBuildTransformError) return true; + if (error instanceof ESBuildBuildError) return true; + if (error instanceof ESBuildContextError) return true; + return false; +} diff --git a/packages/core/src/internal/logger.ts b/packages/core/src/internal/logger.ts index 7ce4e1ddf..adfb147f6 100644 --- a/packages/core/src/internal/logger.ts +++ b/packages/core/src/internal/logger.ts @@ -2,7 +2,7 @@ import type { Prettify } from "@/types/utils.js"; import { formatEta } from "@/utils/format.js"; import pc from "picocolors"; import { type DestinationStream, type LevelWithSilent, pino } from "pino"; -import { NonRetryableUserError } from "./errors.js"; +import { stripErrorStack } from "./errors.js"; export type LogMode = "pretty" | "json"; export type LogLevel = Prettify; @@ -71,10 +71,16 @@ export function createLogger({ options: T, printKeys?: (keyof T)[], ) { + if (options.msg === undefined) { + console.trace(); + } if (mode === "pretty" && printKeys) { // @ts-ignore options[PRINT_KEYS] = printKeys; } + if (options.error && options.error instanceof Error) { + stripErrorStack(options.error); + } logger.error(options); }, warn>( @@ -85,6 +91,9 @@ export function createLogger({ // @ts-ignore options[PRINT_KEYS] = printKeys; } + if (options.error && options.error instanceof Error) { + stripErrorStack(options.error); + } logger.warn(options); }, info>( @@ -95,6 +104,9 @@ export function createLogger({ // @ts-ignore options[PRINT_KEYS] = printKeys; } + if (options.error && options.error instanceof Error) { + stripErrorStack(options.error); + } logger.info(options); }, debug>( @@ -105,6 +117,10 @@ export function createLogger({ // @ts-ignore options[PRINT_KEYS] = printKeys; } + if (options.error && options.error instanceof Error) { + stripErrorStack(options.error); + } + logger.debug(options); }, trace>( @@ -115,6 +131,9 @@ export function createLogger({ // @ts-ignore options[PRINT_KEYS] = printKeys; } + if (options.error && options.error instanceof Error) { + stripErrorStack(options.error); + } logger.trace(options); }, child: (bindings) => _createLogger(logger.child(bindings)), @@ -236,17 +255,15 @@ const format = (log: Log) => { } if (log.error) { - if (log.error.stack && log.error instanceof NonRetryableUserError) { + if (log.error.stack) { prettyLog.push(log.error.stack); } else { prettyLog.push(`${log.error.name}: ${log.error.message}`); } - if (typeof log.error === "object" && "where" in log.error) { - prettyLog.push(`where: ${log.error.where as string}`); - } if (typeof log.error === "object" && "meta" in log.error) { - prettyLog.push(log.error.meta as string); + // @ts-ignore + prettyLog.push(log.error.meta); } } return prettyLog.join("\n"); diff --git a/packages/core/src/internal/types.ts b/packages/core/src/internal/types.ts index acea95bbb..43af06a88 100644 --- a/packages/core/src/internal/types.ts +++ b/packages/core/src/internal/types.ts @@ -29,7 +29,6 @@ import type { Chain as ViemChain, Log as ViemLog, } from "viem"; -import type { RetryableError } from "./errors.js"; // Database @@ -388,11 +387,14 @@ export type Status = { // Indexing error handler +/** + * Object to prevent user code from swallowing internal ponder errors. + */ export type IndexingErrorHandler = { - getRetryableError: () => RetryableError | undefined; - setRetryableError: (error: RetryableError) => void; - clearRetryableError: () => void; - error: RetryableError | undefined; + getError: () => Error | undefined; + setError: (error: Error) => void; + clearError: () => void; + error: Error | undefined; }; // Seconds diff --git a/packages/core/src/rpc/index.ts b/packages/core/src/rpc/index.ts index 2e2f36ec1..ad5b66cbb 100644 --- a/packages/core/src/rpc/index.ts +++ b/packages/core/src/rpc/index.ts @@ -1,7 +1,7 @@ import crypto, { type UUID } from "node:crypto"; import url from "node:url"; import type { Common } from "@/internal/common.js"; -import { RpcRequestError } from "@/internal/errors.js"; +import { EthGetLogsRangeError, RpcRequestError } from "@/internal/errors.js"; import type { Logger } from "@/internal/logger.js"; import type { Chain, SyncBlock, SyncBlockHeader } from "@/internal/types.js"; import { eth_getBlockByNumber, standardizeBlock } from "@/rpc/actions.js"; @@ -576,7 +576,10 @@ export const createRpc = ({ error, }); - throw error; + throw new EthGetLogsRangeError( + { cause: error }, + getLogsErrorResponse, + ); } else { error.meta = [ "Tip: Use the ethGetLogsBlockRange option to override the default behavior for this chain", @@ -952,11 +955,10 @@ function shouldRetry(error: Error) { } function shouldRateLimit(error: Error) { - return ( - // @ts-ignore - error.code === 429 || - // @ts-ignore - error.status === 429 || - error instanceof TimeoutError - ); + // @ts-ignore + if (error.code === 429) return true; + // @ts-ignore + if (error.status === 429) return true; + if (error instanceof TimeoutError) return true; + return false; } diff --git a/packages/core/src/runtime/isolated.ts b/packages/core/src/runtime/isolated.ts index 0b16b94f4..93bb69253 100644 --- a/packages/core/src/runtime/isolated.ts +++ b/packages/core/src/runtime/isolated.ts @@ -18,11 +18,7 @@ import { getEventCount, } from "@/indexing/index.js"; import type { Common } from "@/internal/common.js"; -import { - InvalidEventAccessError, - NonRetryableUserError, - type RetryableError, -} from "@/internal/errors.js"; +import { InvalidEventAccessError } from "@/internal/errors.js"; import type { CrashRecoveryCheckpoint, IndexingBuild, @@ -93,16 +89,16 @@ export async function runIsolated({ }); const indexingErrorHandler: IndexingErrorHandler = { - getRetryableError: () => { + getError: () => { return indexingErrorHandler.error; }, - setRetryableError: (error: RetryableError) => { + setError: (error: Error) => { indexingErrorHandler.error = error; }, - clearRetryableError: () => { + clearError: () => { indexingErrorHandler.error = undefined; }, - error: undefined as RetryableError | undefined, + error: undefined as Error | undefined, }; const indexingCache = createIndexingCache({ @@ -421,16 +417,15 @@ export async function runIsolated({ syncStore, events, }); - } else if (error instanceof NonRetryableUserError === false) { - common.logger.warn({ - msg: "Failed to index block range", - chain: chain.name, - chain_id: chain.id, - block_range: JSON.stringify(blockRange), - duration: indexStartClock(), - error: error as Error, - }); } + common.logger.warn({ + msg: "Failed to index block range", + chain: chain.name, + chain_id: chain.id, + block_range: JSON.stringify(blockRange), + duration: indexStartClock(), + error: error as Error, + }); throw error; } @@ -623,15 +618,13 @@ export async function runIsolated({ } catch (error) { indexingCache.clear(); - if (error instanceof NonRetryableUserError === false) { - common.logger.warn({ - msg: "Failed to index block", - chain: chain.name, - chain_id: chain.id, - number: Number(decodeCheckpoint(checkpoint).blockNumber), - error: error, - }); - } + common.logger.warn({ + msg: "Failed to index block", + chain: chain.name, + chain_id: chain.id, + number: Number(decodeCheckpoint(checkpoint).blockNumber), + error: error, + }); throw error; } diff --git a/packages/core/src/runtime/multichain.ts b/packages/core/src/runtime/multichain.ts index 37dbba9fc..fe1437fc0 100644 --- a/packages/core/src/runtime/multichain.ts +++ b/packages/core/src/runtime/multichain.ts @@ -24,11 +24,7 @@ import { getEventCount, } from "@/indexing/index.js"; import type { Common } from "@/internal/common.js"; -import { - InvalidEventAccessError, - NonRetryableUserError, - type RetryableError, -} from "@/internal/errors.js"; +import { InvalidEventAccessError } from "@/internal/errors.js"; import { getAppProgress } from "@/internal/metrics.js"; import type { Chain, @@ -105,16 +101,16 @@ export async function runMultichain({ }); const indexingErrorHandler: IndexingErrorHandler = { - getRetryableError: () => { + getError: () => { return indexingErrorHandler.error; }, - setRetryableError: (error: RetryableError) => { + setError: (error: Error) => { indexingErrorHandler.error = error; }, - clearRetryableError: () => { + clearError: () => { indexingErrorHandler.error = undefined; }, - error: undefined as RetryableError | undefined, + error: undefined as Error | undefined, }; const indexingCache = createIndexingCache({ @@ -476,17 +472,17 @@ export async function runMultichain({ syncStore, events, }); - } else if (error instanceof NonRetryableUserError === false) { - common.logger.warn({ - msg: "Failed to index block range", - chain: chain.name, - chain_id: chain.id, - block_range: JSON.stringify(blockRange), - duration: indexStartClock(), - error: error as Error, - }); } + common.logger.warn({ + msg: "Failed to index block range", + chain: chain.name, + chain_id: chain.id, + block_range: JSON.stringify(blockRange), + duration: indexStartClock(), + error: error as Error, + }); + throw error; } }, @@ -710,15 +706,13 @@ export async function runMultichain({ } catch (error) { indexingCache.clear(); - if (error instanceof NonRetryableUserError === false) { - common.logger.warn({ - msg: "Failed to index block", - chain: chain.name, - chain_id: chain.id, - number: Number(decodeCheckpoint(checkpoint).blockNumber), - error: error, - }); - } + common.logger.warn({ + msg: "Failed to index block", + chain: chain.name, + chain_id: chain.id, + number: Number(decodeCheckpoint(checkpoint).blockNumber), + error: error, + }); throw error; } diff --git a/packages/core/src/runtime/omnichain.ts b/packages/core/src/runtime/omnichain.ts index 093946d73..9044e69e5 100644 --- a/packages/core/src/runtime/omnichain.ts +++ b/packages/core/src/runtime/omnichain.ts @@ -24,11 +24,7 @@ import { getEventCount, } from "@/indexing/index.js"; import type { Common } from "@/internal/common.js"; -import { - InvalidEventAccessError, - NonRetryableUserError, - type RetryableError, -} from "@/internal/errors.js"; +import { InvalidEventAccessError } from "@/internal/errors.js"; import { getAppProgress } from "@/internal/metrics.js"; import type { Chain, @@ -108,16 +104,16 @@ export async function runOmnichain({ }); const indexingErrorHandler: IndexingErrorHandler = { - getRetryableError: () => { + getError: () => { return indexingErrorHandler.error; }, - setRetryableError: (error: RetryableError) => { + setError: (error: Error) => { indexingErrorHandler.error = error; }, - clearRetryableError: () => { + clearError: () => { indexingErrorHandler.error = undefined; }, - error: undefined as RetryableError | undefined, + error: undefined as Error | undefined, }; const indexingCache = createIndexingCache({ @@ -492,13 +488,12 @@ export async function runOmnichain({ syncStore, events, }); - } else if (error instanceof NonRetryableUserError === false) { - common.logger.warn({ - msg: "Failed to index block range", - duration: indexStartClock(), - error: error as Error, - }); } + common.logger.warn({ + msg: "Failed to index block range", + duration: indexStartClock(), + error: error as Error, + }); throw error; } @@ -728,15 +723,13 @@ export async function runOmnichain({ } catch (error) { indexingCache.clear(); - if (error instanceof NonRetryableUserError === false) { - common.logger.warn({ - msg: "Failed to index block", - chain: chain.name, - chain_id: chain.id, - number: Number(decodeCheckpoint(checkpoint).blockNumber), - error: error, - }); - } + common.logger.warn({ + msg: "Failed to index block", + chain: chain.name, + chain_id: chain.id, + number: Number(decodeCheckpoint(checkpoint).blockNumber), + error: error, + }); throw error; } diff --git a/packages/core/src/server/error.ts b/packages/core/src/server/error.ts index 2b19caf4c..3241116e6 100644 --- a/packages/core/src/server/error.ts +++ b/packages/core/src/server/error.ts @@ -1,12 +1,12 @@ import { addStackTrace } from "@/indexing/addStackTrace.js"; import type { Common } from "@/internal/common.js"; -import type { BaseError } from "@/internal/errors.js"; +import { ServerError } from "@/internal/errors.js"; import { prettyPrint } from "@/utils/print.js"; import type { Context, HonoRequest } from "hono"; import { html } from "hono/html"; export const onError = async (_error: Error, c: Context, common: Common) => { - const error = _error as BaseError; + const error = new ServerError({ cause: _error }); // Find the filename where the error occurred const regex = /(\S+\.(?:js|ts|mjs|cjs)):\d+:\d+/; diff --git a/packages/core/src/sync-historical/index.ts b/packages/core/src/sync-historical/index.ts index 2f4a334c0..0e1b9e145 100644 --- a/packages/core/src/sync-historical/index.ts +++ b/packages/core/src/sync-historical/index.ts @@ -1,4 +1,5 @@ import type { Common } from "@/internal/common.js"; +import { EthGetLogsRangeError } from "@/internal/errors.js"; import type { BlockFilter, Chain, @@ -54,13 +55,11 @@ import { import { promiseAllSettledWithThrow } from "@/utils/promiseAllSettledWithThrow.js"; import { createQueue } from "@/utils/queue.js"; import { startClock } from "@/utils/timer.js"; -import { getLogsRetryHelper } from "@ponder/utils"; import { type Address, type Hash, type Hex, type LogTopic, - type RpcError, hexToNumber, numberToHex, toHex, @@ -211,42 +210,29 @@ export const createHistoricalSync = ( throw error; } - const getLogsErrorResponse = getLogsRetryHelper({ - params: [ - { - address, - topics, - fromBlock: toHex(interval[0]), - toBlock: toHex(interval[1]), - }, - ], - error: error as RpcError, - }); - - if (getLogsErrorResponse.shouldRetry === false) throw error; + if (error instanceof EthGetLogsRangeError) { + const range = + hexToNumber(error.ranges[0]!.toBlock) - + hexToNumber(error.ranges[0]!.fromBlock); - const range = - hexToNumber(getLogsErrorResponse.ranges[0]!.toBlock) - - hexToNumber(getLogsErrorResponse.ranges[0]!.fromBlock); - - args.common.logger.debug({ - msg: "Updated eth_getLogs range", - chain: args.chain.name, - chain_id: args.chain.id, - range, - }); + args.common.logger.debug({ + msg: "Updated eth_getLogs range", + chain: args.chain.name, + chain_id: args.chain.id, + range, + }); - logsRequestMetadata = { - estimatedRange: range, - confirmedRange: getLogsErrorResponse.isSuggestedRange - ? range - : undefined, - }; + logsRequestMetadata = { + estimatedRange: range, + confirmedRange: error.isSuggestedRange ? range : undefined, + }; - return syncLogsDynamic( - { address, topic0, topic1, topic2, topic3, interval }, - context, - ); + return syncLogsDynamic( + { address, topic0, topic1, topic2, topic3, interval }, + context, + ); + } + throw error; }), ), ), From 5386d873c706f28434218a1f68ad7cc55ea45e6b Mon Sep 17 00:00:00 2001 From: Kyle Scott Date: Wed, 14 Jan 2026 11:26:53 -0500 Subject: [PATCH 06/11] database tests --- packages/core/src/database/actions.test.ts | 9 +- .../core/src/database/queryBuilder.test.ts | 17 ++- packages/core/src/database/queryBuilder.ts | 107 +++++++++--------- 3 files changed, 71 insertions(+), 62 deletions(-) diff --git a/packages/core/src/database/actions.test.ts b/packages/core/src/database/actions.test.ts index 31816056c..18a09f9b3 100644 --- a/packages/core/src/database/actions.test.ts +++ b/packages/core/src/database/actions.test.ts @@ -7,7 +7,6 @@ import { import { buildSchema } from "@/build/schema.js"; import { getReorgTable } from "@/drizzle/kit/index.js"; import { onchainTable, primaryKey } from "@/drizzle/onchain.js"; -import type { RetryableError } from "@/internal/errors.js"; import type { IndexingErrorHandler } from "@/internal/types.js"; import { type Checkpoint, @@ -44,16 +43,16 @@ function createCheckpoint(checkpoint: Partial): string { } const indexingErrorHandler: IndexingErrorHandler = { - getRetryableError: () => { + getError: () => { return indexingErrorHandler.error; }, - setRetryableError: (error: RetryableError) => { + setError: (error: Error) => { indexingErrorHandler.error = error; }, - clearRetryableError: () => { + clearError: () => { indexingErrorHandler.error = undefined; }, - error: undefined as RetryableError | undefined, + error: undefined as Error | undefined, }; test("finalize()", async () => { diff --git a/packages/core/src/database/queryBuilder.test.ts b/packages/core/src/database/queryBuilder.test.ts index a815b71cf..1f6d1d499 100644 --- a/packages/core/src/database/queryBuilder.test.ts +++ b/packages/core/src/database/queryBuilder.test.ts @@ -110,7 +110,7 @@ test("QB transaction retries error", async () => { connection.release(); }); -test.skip("QB parses error", async () => { +test("QB parses error", async () => { if (context.databaseConfig.kind !== "postgres") return; const pool = createPool( @@ -124,12 +124,19 @@ test.skip("QB parses error", async () => { const querySpy = vi.spyOn(pool, "query"); querySpy.mockRejectedValueOnce(new Error("violates not-null constraint")); - const error = await qb - .wrap({ label: "test1" }, (db) => db.select().from(SCHEMATA)) - .catch((error) => error); + await expect( + qb.wrap({ label: "test1" }, (db) => db.select().from(SCHEMATA)), + ).rejects.toThrow(); expect(querySpy).toHaveBeenCalledTimes(1); - expect(error).toBeInstanceOf(NotNullConstraintError); + + await expect( + qb.wrap({ label: "test2" }, (db) => + db.execute("SELECT * FRROM information_schema.schemata"), + ), + ).rejects.toThrow(); + + expect(querySpy).toHaveBeenCalledTimes(2); }); test("QB client", async () => { diff --git a/packages/core/src/database/queryBuilder.ts b/packages/core/src/database/queryBuilder.ts index 64e7cfdd8..57e0bd0a3 100644 --- a/packages/core/src/database/queryBuilder.ts +++ b/packages/core/src/database/queryBuilder.ts @@ -1,6 +1,7 @@ import crypto from "node:crypto"; import type { Common } from "@/internal/common.js"; import { + BigIntSerializationError, QueryBuilderError, ShutdownError, TransactionCallbackError, @@ -55,8 +56,6 @@ type TransactionQB< ): Promise; }; -// TODO(kyle) handle malformed queries - /** * Query builder with built-in retry logic, logging, and metrics. */ @@ -85,52 +84,6 @@ export type QB< | { $dialect: "postgres"; $client: pg.Pool | pg.PoolClient } ); -// export const parseQBError = (error: Error): Error => { -// // TODO(kyle) how to know if the error is a query builder error? - -// // TODO(kyle) do we need this? -// if (error instanceof BaseError) return error; - -// if (error?.message?.includes("violates not-null constraint")) { -// return new NotNullConstraintError(undefined, { cause: error }); -// } else if (error?.message?.includes("violates unique constraint")) { -// return new UniqueConstraintError(undefined, { cause: error }); -// } else if (error?.message?.includes("violates check constraint")) { -// return new CheckConstraintError(undefined, { cause: error }); -// } else if ( -// // nodejs error message -// error?.message?.includes("Do not know how to serialize a BigInt") || -// // bun error message -// error?.message?.includes("cannot serialize BigInt") -// ) { -// const bigIntSerializationError = new BigIntSerializationError(undefined, { -// cause: error, -// }); -// bigIntSerializationError.meta.push( -// "Hint:\n The JSON column type does not support BigInt values. Use the replaceBigInts() helper function before inserting into the database. Docs: https://ponder.sh/docs/api-reference/ponder-utils#replacebigints", -// ); -// return bigIntSerializationError; -// } else if (error?.message?.includes("does not exist")) { -// return new NonRetryableUserError(error.message); -// } else if (error?.message?.includes("already exists")) { -// return new NonRetryableUserError(error.message); -// } else if ( -// error?.message?.includes( -// "terminating connection due to administrator command", -// ) || -// error?.message?.includes("connection to client lost") || -// error?.message?.includes("too many clients already") || -// error?.message?.includes("Connection terminated unexpectedly") || -// error?.message?.includes("ECONNRESET") || -// error?.message?.includes("ETIMEDOUT") || -// error?.message?.includes("timeout exceeded when trying to connect") -// ) { -// return new DbConnectionError(error.message); -// } - -// return error; -// }; - /** * Create a query builder. * @@ -260,7 +213,16 @@ export const createQB = < throw error.cause.cause; } - // TODO(kyle) shouldRetry() + if (shouldRetry(error.cause) === false) { + logger.warn({ + msg: "Failed database query", + query: label, + query_id: id, + duration: endClock(), + error, + }); + throw error; + } if (i === RETRY_COUNT) { logger.warn({ @@ -583,6 +545,47 @@ export const createQB = < return qb; }; -// function shouldRetry(error: Error) { -// return true; -// } +function shouldRetry(error: Error) { + if (error?.message?.includes("violates not-null constraint")) { + return false; + } + if (error?.message?.includes("violates unique constraint")) { + return false; + } + if (error?.message?.includes("violates check constraint")) { + return false; + } + if ( + error instanceof BigIntSerializationError || + // nodejs error message + error?.message?.includes("Do not know how to serialize a BigInt") || + // bun error message + error?.message?.includes("cannot serialize BigInt") + ) { + return false; + } + if (error?.message?.includes("does not exist")) { + return false; + } + if (error?.message?.includes("already exists")) { + return false; + } + if (error?.message?.includes("syntax error")) { + return false; + } + + // if ( + // error?.message?.includes( + // "terminating connection due to administrator command", + // ) || + // error?.message?.includes("connection to client lost") || + // error?.message?.includes("too many clients already") || + // error?.message?.includes("Connection terminated unexpectedly") || + // error?.message?.includes("ECONNRESET") || + // error?.message?.includes("ETIMEDOUT") || + // error?.message?.includes("timeout exceeded when trying to connect") + // ) { + // } + + return true; +} From 4249985021677dd713075b1223efa776f8a5ad25 Mon Sep 17 00:00:00 2001 From: Kyle Scott Date: Wed, 14 Jan 2026 12:16:03 -0500 Subject: [PATCH 07/11] fix tests --- packages/core/src/database/index.test.ts | 9 ++-- packages/core/src/database/index.ts | 17 ++++--- packages/core/src/database/queryBuilder.ts | 6 ++- packages/core/src/index.ts | 4 +- .../core/src/indexing-store/cache.test.ts | 14 +++--- .../core/src/indexing-store/index.test.ts | 47 +++++++++---------- packages/core/src/indexing/index.test.ts | 13 ++--- packages/core/src/internal/errors.ts | 23 --------- packages/core/src/internal/logger.ts | 25 +++++++++- packages/core/src/rpc/index.ts | 10 +++- packages/core/src/runtime/historical.ts | 17 ------- 11 files changed, 84 insertions(+), 101 deletions(-) diff --git a/packages/core/src/database/index.test.ts b/packages/core/src/database/index.test.ts index c3512d527..fc969fade 100644 --- a/packages/core/src/database/index.test.ts +++ b/packages/core/src/database/index.test.ts @@ -7,7 +7,6 @@ import { onchainView, primaryKey, } from "@/drizzle/onchain.js"; -import type { RetryableError } from "@/internal/errors.js"; import { createShutdown } from "@/internal/shutdown.js"; import type { IndexingErrorHandler } from "@/internal/types.js"; import { @@ -55,16 +54,16 @@ function createCheckpoint(checkpoint: Partial): string { } const indexingErrorHandler: IndexingErrorHandler = { - getRetryableError: () => { + getError: () => { return indexingErrorHandler.error; }, - setRetryableError: (error: RetryableError) => { + setError: (error: Error) => { indexingErrorHandler.error = error; }, - clearRetryableError: () => { + clearError: () => { indexingErrorHandler.error = undefined; }, - error: undefined as RetryableError | undefined, + error: undefined as Error | undefined, }; test("migrate() succeeds with empty schema", async () => { diff --git a/packages/core/src/database/index.ts b/packages/core/src/database/index.ts index 553608247..42c6a0d5a 100644 --- a/packages/core/src/database/index.ts +++ b/packages/core/src/database/index.ts @@ -48,7 +48,7 @@ import { dropLiveQueryTriggers, dropTriggers, } from "./actions.js"; -import { type QB, createQB } from "./queryBuilder.js"; +import { type QB, createQB, shouldRetry } from "./queryBuilder.js"; export type Database = { driver: PostgresDriver | PGliteDriver; @@ -468,14 +468,13 @@ export const createDatabase = ({ method: "migrate_sync", }); - common.logger.warn({ - msg: "Failed database query", - query: "migrate_sync", - retry_count: i, - error, - }); - - // TODO(kyle) shouldRetry() + if (shouldRetry(error.cause) === false) { + common.logger.warn({ + msg: "Failed database query", + query: "migrate_sync", + error, + }); + } if (i === 9) { common.logger.warn({ diff --git a/packages/core/src/database/queryBuilder.ts b/packages/core/src/database/queryBuilder.ts index 57e0bd0a3..36e075872 100644 --- a/packages/core/src/database/queryBuilder.ts +++ b/packages/core/src/database/queryBuilder.ts @@ -1,6 +1,7 @@ import crypto from "node:crypto"; import type { Common } from "@/internal/common.js"; import { + BaseError, BigIntSerializationError, QueryBuilderError, ShutdownError, @@ -545,7 +546,7 @@ export const createQB = < return qb; }; -function shouldRetry(error: Error) { +export function shouldRetry(error: Error) { if (error?.message?.includes("violates not-null constraint")) { return false; } @@ -573,6 +574,9 @@ function shouldRetry(error: Error) { if (error?.message?.includes("syntax error")) { return false; } + if (error instanceof BaseError && error.cause) { + if (shouldRetry(error.cause) === false) return false; + } // if ( // error?.message?.includes( diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index a4efda515..3b1f11d53 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -35,6 +35,8 @@ export { primaryKey, hex, bigint, + json, + jsonb, } from "@/drizzle/onchain.js"; export type { @@ -112,8 +114,6 @@ export { inet, integer, interval, - json, - jsonb, line, macaddr, macaddr8, diff --git a/packages/core/src/indexing-store/cache.test.ts b/packages/core/src/indexing-store/cache.test.ts index 8b8a657be..777cd9d6d 100644 --- a/packages/core/src/indexing-store/cache.test.ts +++ b/packages/core/src/indexing-store/cache.test.ts @@ -11,7 +11,7 @@ import { deployErc20, mintErc20 } from "@/_test/simulate.js"; import { getErc20IndexingBuild, getSimulatedEvent } from "@/_test/utils.js"; import { onchainEnum, onchainTable } from "@/drizzle/onchain.js"; import { getEventCount } from "@/indexing/index.js"; -import type { RetryableError } from "@/internal/errors.js"; +import { DelayedInsertError } from "@/internal/errors.js"; import type { IndexingErrorHandler } from "@/internal/types.js"; import { parseEther, zeroAddress } from "viem"; import { beforeEach, expect, test } from "vitest"; @@ -24,16 +24,16 @@ beforeEach(setupCleanup); beforeEach(setupAnvil); const indexingErrorHandler: IndexingErrorHandler = { - getRetryableError: () => { + getError: () => { return indexingErrorHandler.error; }, - setRetryableError: (error: RetryableError) => { + setError: (error: Error) => { indexingErrorHandler.error = error; }, - clearRetryableError: () => { + clearError: () => { indexingErrorHandler.error = undefined; }, - error: undefined as RetryableError | undefined, + error: undefined as Error | undefined, }; test("flush() insert", async () => { @@ -215,9 +215,7 @@ test("flush() recovers error", async () => { balance: 10n, }); - await expect(indexingCache.flush()).rejects.toThrowError( - `duplicate key value violates unique constraint "account_pkey"`, - ); + await expect(indexingCache.flush()).rejects.toThrow(DelayedInsertError); }); }); diff --git a/packages/core/src/indexing-store/index.test.ts b/packages/core/src/indexing-store/index.test.ts index f7dac125f..7009fa385 100644 --- a/packages/core/src/indexing-store/index.test.ts +++ b/packages/core/src/indexing-store/index.test.ts @@ -10,9 +10,8 @@ import { getRejectionValue } from "@/_test/utils.js"; import { onchainEnum, onchainTable } from "@/drizzle/onchain.js"; import { BigIntSerializationError, - NonRetryableUserError, + IndexingDBError, RawSqlError, - type RetryableError, } from "@/internal/errors.js"; import type { IndexingErrorHandler } from "@/internal/types.js"; import { eq } from "drizzle-orm"; @@ -27,16 +26,16 @@ beforeEach(setupIsolatedDatabase); beforeEach(setupCleanup); const indexingErrorHandler: IndexingErrorHandler = { - getRetryableError: () => { + getError: () => { return indexingErrorHandler.error; }, - setRetryableError: (error: RetryableError) => { + setError: (error: Error) => { indexingErrorHandler.error = error; }, - clearRetryableError: () => { + clearError: () => { indexingErrorHandler.error = undefined; }, - error: undefined as RetryableError | undefined, + error: undefined as Error | undefined, }; test("find", async () => { @@ -415,7 +414,7 @@ test("update throw error when primary key is updated", async () => { .set({ address: ALICE }) .catch((error) => error); - expect(error).toBeInstanceOf(NonRetryableUserError); + expect(error).toBeInstanceOf(IndexingDBError); // function @@ -424,7 +423,7 @@ test("update throw error when primary key is updated", async () => { .set(() => ({ address: ALICE })) .catch((error) => error); - expect(error).toBeInstanceOf(NonRetryableUserError); + expect(error).toBeInstanceOf(IndexingDBError); // update same primary key no function let row: any = await indexingStore.db @@ -566,16 +565,15 @@ test("sql", async () => { await tx.wrap((db) => db.execute("SAVEPOINT test")); - expect( - await getRejectionValue( - async () => - // @ts-ignore - await indexingStore.db.sql.insert(schema.account).values({ - address: "0x0000000000000000000000000000000000000001", - balance: undefined, - }), - ), - ).toBeInstanceOf(RawSqlError); + await expect( + // @ts-ignore + indexingStore.db.sql + .insert(schema.account) + .values({ + address: "0x0000000000000000000000000000000000000001", + balance: undefined, + }), + ).rejects.toThrow(RawSqlError); // TODO(kyle) check constraint @@ -583,14 +581,11 @@ test("sql", async () => { await tx.wrap((db) => db.execute("ROLLBACK TO test")); - expect( - await getRejectionValue( - async () => - await indexingStore.db.sql - .insert(schema.account) - .values({ address: zeroAddress, balance: 10n }), - ), - ).toBeInstanceOf(RawSqlError); + await expect( + indexingStore.db.sql + .insert(schema.account) + .values({ address: zeroAddress, balance: 10n }), + ).rejects.toThrow(RawSqlError); }); }); diff --git a/packages/core/src/indexing/index.test.ts b/packages/core/src/indexing/index.test.ts index 180fd8ea2..f63e8aa5d 100644 --- a/packages/core/src/indexing/index.test.ts +++ b/packages/core/src/indexing/index.test.ts @@ -18,10 +18,7 @@ import { onchainTable } from "@/drizzle/onchain.js"; import { createIndexingCache } from "@/indexing-store/cache.js"; import { createIndexingStore } from "@/indexing-store/index.js"; import { createCachedViemClient } from "@/indexing/client.js"; -import { - InvalidEventAccessError, - type RetryableError, -} from "@/internal/errors.js"; +import { InvalidEventAccessError } from "@/internal/errors.js"; import type { IndexingErrorHandler } from "@/internal/types.js"; import { createRpc } from "@/rpc/index.js"; import { parseEther, toHex, zeroAddress } from "viem"; @@ -47,16 +44,16 @@ const account = onchainTable("account", (p) => ({ const schema = { account }; const indexingErrorHandler: IndexingErrorHandler = { - getRetryableError: () => { + getError: () => { return indexingErrorHandler.error; }, - setRetryableError: (error: RetryableError) => { + setError: (error: Error) => { indexingErrorHandler.error = error; }, - clearRetryableError: () => { + clearError: () => { indexingErrorHandler.error = undefined; }, - error: undefined as RetryableError | undefined, + error: undefined as Error | undefined, }; test("createIndexing()", async () => { diff --git a/packages/core/src/internal/errors.ts b/packages/core/src/internal/errors.ts index 9528bc856..b4689547b 100644 --- a/packages/core/src/internal/errors.ts +++ b/packages/core/src/internal/errors.ts @@ -1,8 +1,3 @@ -import { - ESBuildBuildError, - ESBuildContextError, - ESBuildTransformError, -} from "@/build/stacktrace.js"; import type { getLogsRetryHelper } from "@ponder/utils"; /** Base class for all known errors. */ @@ -266,21 +261,3 @@ export function isUserDerivedError(error: BaseError): boolean { } return false; } - -export function stripErrorStack(error: Error): void { - if (shouldPrintErrorStack(error) === false) { - error.stack = undefined; - } - if (error instanceof BaseError && error.cause) { - stripErrorStack(error.cause); - } -} - -export function shouldPrintErrorStack(error: Error): boolean { - if (error instanceof ServerError) return true; - if (error instanceof IndexingFunctionError) return true; - if (error instanceof ESBuildTransformError) return true; - if (error instanceof ESBuildBuildError) return true; - if (error instanceof ESBuildContextError) return true; - return false; -} diff --git a/packages/core/src/internal/logger.ts b/packages/core/src/internal/logger.ts index adfb147f6..c728eeb3b 100644 --- a/packages/core/src/internal/logger.ts +++ b/packages/core/src/internal/logger.ts @@ -1,8 +1,13 @@ +import { + ESBuildBuildError, + ESBuildContextError, + ESBuildTransformError, +} from "@/build/stacktrace.js"; import type { Prettify } from "@/types/utils.js"; import { formatEta } from "@/utils/format.js"; import pc from "picocolors"; import { type DestinationStream, type LevelWithSilent, pino } from "pino"; -import { stripErrorStack } from "./errors.js"; +import { BaseError, IndexingFunctionError, ServerError } from "./errors.js"; export type LogMode = "pretty" | "json"; export type LogLevel = Prettify; @@ -268,3 +273,21 @@ const format = (log: Log) => { } return prettyLog.join("\n"); }; + +function stripErrorStack(error: Error): void { + if (shouldPrintErrorStack(error) === false) { + error.stack = undefined; + } + if (error instanceof BaseError && error.cause) { + stripErrorStack(error.cause); + } +} + +function shouldPrintErrorStack(error: Error): boolean { + if (error instanceof ServerError) return true; + if (error instanceof IndexingFunctionError) return true; + if (error instanceof ESBuildTransformError) return true; + if (error instanceof ESBuildBuildError) return true; + if (error instanceof ESBuildContextError) return true; + return false; +} diff --git a/packages/core/src/rpc/index.ts b/packages/core/src/rpc/index.ts index ad5b66cbb..43f3d5fed 100644 --- a/packages/core/src/rpc/index.ts +++ b/packages/core/src/rpc/index.ts @@ -1,7 +1,11 @@ import crypto, { type UUID } from "node:crypto"; import url from "node:url"; import type { Common } from "@/internal/common.js"; -import { EthGetLogsRangeError, RpcRequestError } from "@/internal/errors.js"; +import { + EthGetLogsRangeError, + RpcRequestError, + ShutdownError, +} from "@/internal/errors.js"; import type { Logger } from "@/internal/logger.js"; import type { Chain, SyncBlock, SyncBlockHeader } from "@/internal/types.js"; import { eth_getBlockByNumber, standardizeBlock } from "@/rpc/actions.js"; @@ -547,6 +551,10 @@ export const createRpc = ({ cause: _error as Error, }); + if (common.shutdown.isKilled) { + throw new ShutdownError(); + } + common.metrics.ponder_rpc_request_error_total.inc( { method: body.method, chain: chain.name }, 1, diff --git a/packages/core/src/runtime/historical.ts b/packages/core/src/runtime/historical.ts index f551cbc24..39a8e8cdd 100644 --- a/packages/core/src/runtime/historical.ts +++ b/packages/core/src/runtime/historical.ts @@ -1330,23 +1330,6 @@ export async function* getLocalSyncGenerator(params: { return closestToTipBlock; }, ); - // TODO(kyle) don't log here because it's not a decision boundary, instead - // add context to inner errors. - // .catch((error) => { - // if (error instanceof ShutdownError) { - // throw error; - // } - - // params.common.logger.warn({ - // msg: "Failed to fetch backfill JSON-RPC data", - // chain: params.chain.name, - // chain_id: params.chain.id, - // block_range: JSON.stringify(interval), - // duration: endClock(), - // error, - // }); - // throw error; - // }); clearTimeout(durationTimer); From a46549ab71f1240536f8eee6e2e605885623d6a5 Mon Sep 17 00:00:00 2001 From: Kyle Scott Date: Wed, 14 Jan 2026 12:23:19 -0500 Subject: [PATCH 08/11] cleanup --- packages/core/src/bin/commands/dev.ts | 2 -- packages/core/src/indexing-store/index.test.ts | 6 ++++-- packages/core/src/rpc/actions.ts | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/core/src/bin/commands/dev.ts b/packages/core/src/bin/commands/dev.ts index 76e4f16e8..6acc9ac50 100644 --- a/packages/core/src/bin/commands/dev.ts +++ b/packages/core/src/bin/commands/dev.ts @@ -110,8 +110,6 @@ export async function dev({ cliOptions }: { cliOptions: CliOptions }) { // common.logger.error({ error: result.error }); // } - console.log(result.kind); - // This handles indexing function build failures on hot reload. metrics.hasError = true; return; diff --git a/packages/core/src/indexing-store/index.test.ts b/packages/core/src/indexing-store/index.test.ts index 7009fa385..8f12ca057 100644 --- a/packages/core/src/indexing-store/index.test.ts +++ b/packages/core/src/indexing-store/index.test.ts @@ -572,7 +572,8 @@ test("sql", async () => { .values({ address: "0x0000000000000000000000000000000000000001", balance: undefined, - }), + }) + .then((res) => res), ).rejects.toThrow(RawSqlError); // TODO(kyle) check constraint @@ -584,7 +585,8 @@ test("sql", async () => { await expect( indexingStore.db.sql .insert(schema.account) - .values({ address: zeroAddress, balance: 10n }), + .values({ address: zeroAddress, balance: 10n }) + .then((res) => res), ).rejects.toThrow(RawSqlError); }); }); diff --git a/packages/core/src/rpc/actions.ts b/packages/core/src/rpc/actions.ts index f943ab071..df17e3c40 100644 --- a/packages/core/src/rpc/actions.ts +++ b/packages/core/src/rpc/actions.ts @@ -931,7 +931,6 @@ export const standardizeLogs = ( requestText(request), ]; - console.log(logs); throw error; } else { logIds.add(`${log.blockNumber}_${log.logIndex}`); From 65b685a498d806d67249907df90bc78d562ebc7c Mon Sep 17 00:00:00 2001 From: Kyle Scott Date: Wed, 14 Jan 2026 12:56:49 -0500 Subject: [PATCH 09/11] fix error logging --- packages/core/src/internal/logger.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/core/src/internal/logger.ts b/packages/core/src/internal/logger.ts index c728eeb3b..9e0f00f10 100644 --- a/packages/core/src/internal/logger.ts +++ b/packages/core/src/internal/logger.ts @@ -85,6 +85,7 @@ export function createLogger({ } if (options.error && options.error instanceof Error) { stripErrorStack(options.error); + populateErrorMessageAndStack; } logger.error(options); }, @@ -98,7 +99,9 @@ export function createLogger({ } if (options.error && options.error instanceof Error) { stripErrorStack(options.error); + populateErrorMessageAndStack(options.error); } + logger.warn(options); }, info>( @@ -111,6 +114,7 @@ export function createLogger({ } if (options.error && options.error instanceof Error) { stripErrorStack(options.error); + populateErrorMessageAndStack; } logger.info(options); }, @@ -124,6 +128,7 @@ export function createLogger({ } if (options.error && options.error instanceof Error) { stripErrorStack(options.error); + populateErrorMessageAndStack; } logger.debug(options); @@ -138,6 +143,7 @@ export function createLogger({ } if (options.error && options.error instanceof Error) { stripErrorStack(options.error); + populateErrorMessageAndStack; } logger.trace(options); }, @@ -283,6 +289,18 @@ function stripErrorStack(error: Error): void { } } +function populateErrorMessageAndStack(error: Error): void { + if (error.message === undefined || error.message === "") { + error.message = error.name; + } + if (error.stack === undefined) { + error.stack = error.message; + } + if (error instanceof BaseError && error.cause) { + populateErrorMessageAndStack(error.cause); + } +} + function shouldPrintErrorStack(error: Error): boolean { if (error instanceof ServerError) return true; if (error instanceof IndexingFunctionError) return true; From 4f8e113a0a57afac80ae89483e0269bd44ae3b2b Mon Sep 17 00:00:00 2001 From: Kyle Scott Date: Wed, 14 Jan 2026 13:15:31 -0500 Subject: [PATCH 10/11] fix more errors --- packages/core/src/build/index.ts | 152 +++++++++++++++------------ packages/core/src/internal/errors.ts | 12 +++ packages/core/src/internal/logger.ts | 8 +- 3 files changed, 104 insertions(+), 68 deletions(-) diff --git a/packages/core/src/build/index.ts b/packages/core/src/build/index.ts index ff08801b8..1c26050d9 100644 --- a/packages/core/src/build/index.ts +++ b/packages/core/src/build/index.ts @@ -6,7 +6,7 @@ import type { Config } from "@/config/index.js"; import type { Database } from "@/database/index.js"; import { MAX_DATABASE_OBJECT_NAME_LENGTH } from "@/drizzle/onchain.js"; import type { Common } from "@/internal/common.js"; -import { BuildError } from "@/internal/errors.js"; +import { BuildError, ExecuteFileError } from "@/internal/errors.js"; import type { ApiBuild, IndexingBuild, @@ -178,9 +178,11 @@ export const createBuild = async ({ return { status: "success", exports } as const; } catch (error_) { const relativePath = path.relative(common.options.rootDir, file); - const error = new BuildError(undefined, { - cause: parseViteNodeError(relativePath, error_ as Error), + const viteError = parseViteNodeError(relativePath, error_ as Error); + const error = new ExecuteFileError(undefined, { + cause: viteError, }); + error.stack = viteError.stack; return { status: "error", error } as const; } }; @@ -392,46 +394,58 @@ export const createBuild = async ({ } as const; }, preCompile({ config }): Result { - const preBuild = buildPre({ - config, - options: common.options, - logger: common.logger, - }); + try { + const preBuild = buildPre({ + config, + options: common.options, + logger: common.logger, + }); - return { - status: "success", - result: { - databaseConfig: preBuild.databaseConfig, - ordering: preBuild.ordering, - }, - } as const; + return { + status: "success", + result: { + databaseConfig: preBuild.databaseConfig, + ordering: preBuild.ordering, + }, + } as const; + } catch (error) { + return { status: "error", error: error as Error } as const; + } }, compileSchema({ schema, preBuild }) { - const { statements } = buildSchema({ schema, preBuild }); - - return { - status: "success", - result: { schema, statements }, - } as const; + try { + const { statements } = buildSchema({ schema, preBuild }); + + return { + status: "success", + result: { schema, statements }, + } as const; + } catch (error) { + return { status: "error", error: error as Error } as const; + } }, compileConfig({ configResult }) { - const buildConfigResult = buildConfig({ - common, - config: configResult.config, - }); + try { + const buildConfigResult = buildConfig({ + common, + config: configResult.config, + }); - for (const log of buildConfigResult.logs) { - const { level, ...rest } = log; - common.logger[level](rest); - } + for (const log of buildConfigResult.logs) { + const { level, ...rest } = log; + common.logger[level](rest); + } - return { - status: "success", - result: { - chains: buildConfigResult.chains, - rpcs: buildConfigResult.rpcs, - }, - } as const; + return { + status: "success", + result: { + chains: buildConfigResult.chains, + rpcs: buildConfigResult.rpcs, + }, + } as const; + } catch (error) { + return { status: "error", error: error as Error } as const; + } }, async compileIndexing({ configResult, @@ -439,39 +453,43 @@ export const createBuild = async ({ indexingResult, configBuild, }) { - const buildIndexingFunctionsResult = await buildIndexingFunctions({ - common, - config: configResult.config, - indexingFunctions: indexingResult.indexingFunctions, - configBuild, - }); - - for (const log of buildIndexingFunctionsResult.logs) { - const { level, ...rest } = log; - common.logger[level](rest); - } + try { + const buildIndexingFunctionsResult = await buildIndexingFunctions({ + common, + config: configResult.config, + indexingFunctions: indexingResult.indexingFunctions, + configBuild, + }); - const buildId = createHash("sha256") - .update(BUILD_ID_VERSION) - .update(configResult.contentHash) - .update(schemaResult.contentHash) - .update(indexingResult.contentHash) - .digest("hex") - .slice(0, 10); + for (const log of buildIndexingFunctionsResult.logs) { + const { level, ...rest } = log; + common.logger[level](rest); + } - return { - status: "success", - result: { - buildId, - chains: buildIndexingFunctionsResult.chains, - rpcs: buildIndexingFunctionsResult.rpcs, - finalizedBlocks: buildIndexingFunctionsResult.finalizedBlocks, - eventCallbacks: buildIndexingFunctionsResult.eventCallbacks, - setupCallbacks: buildIndexingFunctionsResult.setupCallbacks, - contracts: buildIndexingFunctionsResult.contracts, - indexingFunctions: indexingResult.indexingFunctions, - }, - } as const; + const buildId = createHash("sha256") + .update(BUILD_ID_VERSION) + .update(configResult.contentHash) + .update(schemaResult.contentHash) + .update(indexingResult.contentHash) + .digest("hex") + .slice(0, 10); + + return { + status: "success", + result: { + buildId, + chains: buildIndexingFunctionsResult.chains, + rpcs: buildIndexingFunctionsResult.rpcs, + finalizedBlocks: buildIndexingFunctionsResult.finalizedBlocks, + eventCallbacks: buildIndexingFunctionsResult.eventCallbacks, + setupCallbacks: buildIndexingFunctionsResult.setupCallbacks, + contracts: buildIndexingFunctionsResult.contracts, + indexingFunctions: indexingResult.indexingFunctions, + }, + } as const; + } catch (error) { + return { status: "error", error: error as Error } as const; + } }, async compileApi({ apiResult }) { for (const route of apiResult.app.routes) { diff --git a/packages/core/src/internal/errors.ts b/packages/core/src/internal/errors.ts index b4689547b..ff113b2c5 100644 --- a/packages/core/src/internal/errors.ts +++ b/packages/core/src/internal/errors.ts @@ -36,6 +36,17 @@ export class BuildError< } } +export class ExecuteFileError< + cause extends Error | undefined = undefined, +> extends BaseError { + override name = "ExecuteFileError"; + + constructor(message?: string | undefined, { cause }: { cause?: cause } = {}) { + super(message, { cause }); + Object.setPrototypeOf(this, ExecuteFileError.prototype); + } +} + export class RpcRequestError< cause extends Error | undefined = undefined, > extends BaseError { @@ -247,6 +258,7 @@ export class MigrationError< */ export function isUserDerivedError(error: BaseError): boolean { if (error instanceof BuildError) return true; + if (error instanceof ExecuteFileError) return true; if (error instanceof IndexingDBError) return true; if (error instanceof DelayedInsertError) return true; if ( diff --git a/packages/core/src/internal/logger.ts b/packages/core/src/internal/logger.ts index 9e0f00f10..d23a69c80 100644 --- a/packages/core/src/internal/logger.ts +++ b/packages/core/src/internal/logger.ts @@ -7,7 +7,12 @@ import type { Prettify } from "@/types/utils.js"; import { formatEta } from "@/utils/format.js"; import pc from "picocolors"; import { type DestinationStream, type LevelWithSilent, pino } from "pino"; -import { BaseError, IndexingFunctionError, ServerError } from "./errors.js"; +import { + BaseError, + ExecuteFileError, + IndexingFunctionError, + ServerError, +} from "./errors.js"; export type LogMode = "pretty" | "json"; export type LogLevel = Prettify; @@ -304,6 +309,7 @@ function populateErrorMessageAndStack(error: Error): void { function shouldPrintErrorStack(error: Error): boolean { if (error instanceof ServerError) return true; if (error instanceof IndexingFunctionError) return true; + if (error instanceof ExecuteFileError) return true; if (error instanceof ESBuildTransformError) return true; if (error instanceof ESBuildBuildError) return true; if (error instanceof ESBuildContextError) return true; From 6419e18eb3eab02d6cab5cf972acc10ad728b439 Mon Sep 17 00:00:00 2001 From: Kyle Scott Date: Thu, 15 Jan 2026 11:28:16 -0500 Subject: [PATCH 11/11] cleanup --- packages/core/src/bin/commands/dev.ts | 24 +++--- packages/core/src/bin/commands/start.ts | 20 +++-- packages/core/src/bin/isolatedController.ts | 20 +---- packages/core/src/bin/utils/exit.ts | 4 +- packages/core/src/database/index.ts | 4 +- packages/core/src/database/queryBuilder.ts | 30 +++---- packages/core/src/drizzle/json.ts | 4 +- packages/core/src/indexing-store/cache.ts | 20 +++-- packages/core/src/indexing-store/index.ts | 2 +- packages/core/src/indexing/client.ts | 2 +- packages/core/src/indexing/index.ts | 14 ++-- packages/core/src/internal/errors.ts | 86 +++++++-------------- packages/core/src/runtime/isolated.ts | 25 +++--- packages/core/src/runtime/multichain.ts | 26 +++---- packages/core/src/runtime/omnichain.ts | 16 ++-- packages/core/src/server/error.ts | 2 +- 16 files changed, 135 insertions(+), 164 deletions(-) diff --git a/packages/core/src/bin/commands/dev.ts b/packages/core/src/bin/commands/dev.ts index 6acc9ac50..373889886 100644 --- a/packages/core/src/bin/commands/dev.ts +++ b/packages/core/src/bin/commands/dev.ts @@ -106,10 +106,6 @@ export async function dev({ cliOptions }: { cliOptions: CliOptions }) { } if (result.status === "error") { - // if (isInitialBuild === false) { - // common.logger.error({ error: result.error }); - // } - // This handles indexing function build failures on hot reload. metrics.hasError = true; return; @@ -280,11 +276,21 @@ export async function dev({ cliOptions }: { cliOptions: CliOptions }) { return; } - crashRecoveryCheckpoint = await database.migrate({ - buildId: indexingBuildResult.result.buildId, - chains: indexingBuildResult.result.chains, - finalizedBlocks: indexingBuildResult.result.finalizedBlocks, - }); + crashRecoveryCheckpoint = await database + .migrate({ + buildId: indexingBuildResult.result.buildId, + chains: indexingBuildResult.result.chains, + finalizedBlocks: indexingBuildResult.result.finalizedBlocks, + }) + .catch((error) => { + common.logger.error({ + msg: "Database migration failed", + stage: "migration", + error: error as Error, + }); + + throw error; + }); await database.migrateSync(); diff --git a/packages/core/src/bin/commands/start.ts b/packages/core/src/bin/commands/start.ts index 95a69ab19..16c7140cb 100644 --- a/packages/core/src/bin/commands/start.ts +++ b/packages/core/src/bin/commands/start.ts @@ -227,11 +227,21 @@ export async function start({ return; } - const crashRecoveryCheckpoint = await database.migrate({ - buildId: indexingBuildResult.result.buildId, - chains: indexingBuildResult.result.chains, - finalizedBlocks: indexingBuildResult.result.finalizedBlocks, - }); + const crashRecoveryCheckpoint = await database + .migrate({ + buildId: indexingBuildResult.result.buildId, + chains: indexingBuildResult.result.chains, + finalizedBlocks: indexingBuildResult.result.finalizedBlocks, + }) + .catch((error) => { + common.logger.error({ + msg: "Database migration failed", + stage: "migration", + error: error as Error, + }); + + throw error; + }); await database.migrateSync(); diff --git a/packages/core/src/bin/isolatedController.ts b/packages/core/src/bin/isolatedController.ts index 291108d55..99c1b66f3 100644 --- a/packages/core/src/bin/isolatedController.ts +++ b/packages/core/src/bin/isolatedController.ts @@ -245,34 +245,18 @@ export async function isolatedController({ break; } case "error": { - const error: Error = message.error; - // if (nonRetryableUserErrorNames.includes(message.error.name)) { - // error = new NonRetryableUserError(message.error.message); - // } else { - // error = new Error(message.error.message); - // } - // error = message.error; - // error.name = message.error.name; - // error.stack = message.error.stack; - throw error; + throw message.error; } } }, ); worker.on("error", (error: Error) => { - // if (nonRetryableUserErrorNames.includes(error.name)) { - // error = new NonRetryableUserError(error.message); - // } else { - // error = new Error(error.message); - // } throw error; }); worker.on("exit", (code: number) => { - const error = new Error(`Worker thread exited with code ${code}.`); - error.stack = undefined; - throw error; + throw new Error(`Worker thread exited with code ${code}.`); }); perThreadWorkers.push(worker); diff --git a/packages/core/src/bin/utils/exit.ts b/packages/core/src/bin/utils/exit.ts index 10b81cb22..f341ce87e 100644 --- a/packages/core/src/bin/utils/exit.ts +++ b/packages/core/src/bin/utils/exit.ts @@ -90,7 +90,7 @@ export const createExit = ({ if (error instanceof ShutdownError) return; if (error instanceof BaseError) { common.logger.error({ - msg: `unhandledRejection: ${error.name}`, + msg: `unhandledRejection: ${error.name} ${error.message}`, }); if (isUserDerivedError(error)) { exit({ code: 1 }); @@ -109,7 +109,7 @@ export const createExit = ({ if (error instanceof ShutdownError) return; if (error instanceof BaseError) { common.logger.error({ - msg: `unhandledRejection: ${error.name}`, + msg: `unhandledRejection: ${error.name} ${error.message}`, }); if (isUserDerivedError(error)) { exit({ code: 1 }); diff --git a/packages/core/src/database/index.ts b/packages/core/src/database/index.ts index 42c6a0d5a..a031c7002 100644 --- a/packages/core/src/database/index.ts +++ b/packages/core/src/database/index.ts @@ -454,7 +454,9 @@ export const createDatabase = ({ return; } catch (_error) { - const error = new QueryBuilderError({ cause: _error as Error }); + const error = new QueryBuilderError(undefined, { + cause: _error as Error, + }); if (common.shutdown.isKilled) { throw new ShutdownError(); diff --git a/packages/core/src/database/queryBuilder.ts b/packages/core/src/database/queryBuilder.ts index 36e075872..0821d6ca5 100644 --- a/packages/core/src/database/queryBuilder.ts +++ b/packages/core/src/database/queryBuilder.ts @@ -162,15 +162,9 @@ export const createQB = < return result; } catch (_error) { - const error = new QueryBuilderError({ cause: _error as Error }); - - // TODO(kyle) determine transaction control error? - // if ( - // isTransaction && - // error instanceof TransactionStatementError === false && - // error instanceof TransactionCallbackError === false - // ) { - // } + const error = new QueryBuilderError(undefined, { + cause: _error as Error, + }); if (common.shutdown.isKilled) { throw new ShutdownError(); @@ -208,7 +202,7 @@ export const createQB = < }); // Transaction statements are not immediately retried, so the transaction // will be properly rolled back. - throw new TransactionStatementError({ cause: error }); + throw new TransactionStatementError(undefined, { cause: error }); } else if (error.cause instanceof TransactionCallbackError) { // Unrelated errors are bubbled out of the query builder. throw error.cause.cause; @@ -336,7 +330,9 @@ export const createQB = < if (error instanceof TransactionStatementError) { throw error; } else { - throw new TransactionCallbackError({ cause: error as Error }); + throw new TransactionCallbackError(undefined, { + cause: error as Error, + }); } } }, config), @@ -426,7 +422,9 @@ export const createQB = < if (error instanceof TransactionStatementError) { throw error; } else { - throw new TransactionCallbackError({ cause: error as Error }); + throw new TransactionCallbackError(undefined, { + cause: error as Error, + }); } } }, config), @@ -468,7 +466,9 @@ export const createQB = < if (error instanceof TransactionStatementError) { throw error; } else { - throw new TransactionCallbackError({ cause: error as Error }); + throw new TransactionCallbackError(undefined, { + cause: error as Error, + }); } } }, @@ -501,7 +501,9 @@ export const createQB = < if (error instanceof TransactionStatementError) { throw error; } else { - throw new TransactionCallbackError({ cause: error as Error }); + throw new TransactionCallbackError(undefined, { + cause: error as Error, + }); } } }, diff --git a/packages/core/src/drizzle/json.ts b/packages/core/src/drizzle/json.ts index e8b952077..ed8c6e080 100644 --- a/packages/core/src/drizzle/json.ts +++ b/packages/core/src/drizzle/json.ts @@ -61,7 +61,7 @@ export class PgJson< // bun error message error?.message?.includes("cannot serialize BigInt") ) { - error = new BigIntSerializationError({ cause: error }); + error = new BigIntSerializationError(undefined, { cause: error }); (error as BigIntSerializationError).meta.push( "Hint:\n The JSON column type does not support BigInt values. Use the replaceBigInts() helper function before inserting into the database. Docs: https://ponder.sh/docs/api-reference/ponder-utils#replacebigints", ); @@ -141,7 +141,7 @@ export class PgJsonb< // bun error message error?.message?.includes("cannot serialize BigInt") ) { - error = new BigIntSerializationError({ cause: error }); + error = new BigIntSerializationError(undefined, { cause: error }); (error as BigIntSerializationError).meta.push( "Hint:\n The JSONB column type does not support BigInt values. Use the replaceBigInts() helper function before inserting into the database. Docs: https://ponder.sh/docs/api-reference/ponder-utils#replacebigints", ); diff --git a/packages/core/src/indexing-store/cache.ts b/packages/core/src/indexing-store/cache.ts index 8df765e45..5fc150ea7 100644 --- a/packages/core/src/indexing-store/cache.ts +++ b/packages/core/src/indexing-store/cache.ts @@ -5,9 +5,9 @@ import { getPartitionName } from "@/drizzle/onchain.js"; import { addErrorMeta, toErrorMeta } from "@/indexing/index.js"; import type { Common } from "@/internal/common.js"; import { - CopyFlushError, DelayedInsertError, ShutdownError, + TransactionStatementError, } from "@/internal/errors.js"; import type { CrashRecoveryCheckpoint, @@ -286,7 +286,10 @@ export const getCopyHelper = (qb: QB, chainId?: number) => { blob: new Blob([text]), }) .catch((error) => { - throw new CopyFlushError({ cause: error as Error }); + throw new TransactionStatementError( + `Failed COPY operation for table "${getTableName(table)}"`, + { cause: error as Error }, + ); }); }; } else { @@ -305,7 +308,10 @@ export const getCopyHelper = (qb: QB, chainId?: number) => { copyStream.write(text); copyStream.end(); }).catch((error) => { - throw new CopyFlushError({ cause: error as Error }); + throw new TransactionStatementError( + `Failed COPY operation for table "${getTableName(table)}"`, + { cause: error as Error }, + ); }); }; } @@ -755,7 +761,9 @@ export const createIndexingCache = ({ ); if (result.status === "error") { - error = new DelayedInsertError({ cause: result.error as Error }); + error = new DelayedInsertError(undefined, { + cause: result.error as Error, + }); addErrorMeta( error, @@ -826,7 +834,9 @@ export const createIndexingCache = ({ ); if (result.status === "error") { - error = new DelayedInsertError({ cause: result.error as Error }); + error = new DelayedInsertError(undefined, { + cause: result.error as Error, + }); addErrorMeta( error, diff --git a/packages/core/src/indexing-store/index.ts b/packages/core/src/indexing-store/index.ts index 408ad0d28..eace6a7ba 100644 --- a/packages/core/src/indexing-store/index.ts +++ b/packages/core/src/indexing-store/index.ts @@ -595,7 +595,7 @@ export const createIndexingStore = ({ return result; }); } catch (error) { - throw new RawSqlError({ cause: error as Error }); + throw new RawSqlError(undefined, { cause: error as Error }); } finally { common.metrics.ponder_indexing_store_raw_sql_duration.observe( endClock(), diff --git a/packages/core/src/indexing/client.ts b/packages/core/src/indexing/client.ts index 2873b9442..d70c8a237 100644 --- a/packages/core/src/indexing/client.ts +++ b/packages/core/src/indexing/client.ts @@ -1032,7 +1032,7 @@ export const cachedTransport = functionName: "aggregate3", result: resultsToEncode, }); - } catch (e) { + } catch { return encodeFunctionResult({ abi: multicall3Abi, functionName: "aggregate3", diff --git a/packages/core/src/indexing/index.ts b/packages/core/src/indexing/index.ts index 9e8b025f2..d528598da 100644 --- a/packages/core/src/indexing/index.ts +++ b/packages/core/src/indexing/index.ts @@ -234,12 +234,14 @@ export const createIndexing = ({ let error: IndexingFunctionError; if (indexingErrorHandler.getError()) { - error = new IndexingFunctionError({ + error = new IndexingFunctionError(undefined, { cause: indexingErrorHandler.getError()!, }); indexingErrorHandler.clearError(); } else { - error = new IndexingFunctionError({ cause: _error as Error }); + error = new IndexingFunctionError(undefined, { + cause: _error as Error, + }); } // Copy the stack from the inner error. @@ -259,7 +261,7 @@ export const createIndexing = ({ common.metrics.hasError = true; - throw new IndexingFunctionError({ cause: error as Error }); + throw new IndexingFunctionError(undefined, { cause: error as Error }); } }; @@ -305,12 +307,14 @@ export const createIndexing = ({ let error: IndexingFunctionError; if (indexingErrorHandler.getError()) { - error = new IndexingFunctionError({ + error = new IndexingFunctionError(undefined, { cause: indexingErrorHandler.getError()!, }); indexingErrorHandler.clearError(); } else { - error = new IndexingFunctionError({ cause: _error as Error }); + error = new IndexingFunctionError(undefined, { + cause: _error as Error, + }); } if (error.cause instanceof InvalidEventAccessError) { diff --git a/packages/core/src/internal/errors.ts b/packages/core/src/internal/errors.ts index ff113b2c5..708baaba8 100644 --- a/packages/core/src/internal/errors.ts +++ b/packages/core/src/internal/errors.ts @@ -92,8 +92,8 @@ export class QueryBuilderError< > extends BaseError { override name = "QueryBuilderError"; - constructor({ cause }: { cause?: cause } = {}) { - super(undefined, { cause }); + constructor(message?: string | undefined, { cause }: { cause?: cause } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, QueryBuilderError.prototype); } } @@ -107,8 +107,8 @@ export class TransactionStatementError< > extends BaseError { override name = "TransactionStatementError"; - constructor({ cause }: { cause?: cause } = {}) { - super(undefined, { cause }); + constructor(message?: string | undefined, { cause }: { cause?: cause } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, TransactionStatementError.prototype); } } @@ -121,30 +121,19 @@ export class TransactionCallbackError< > extends BaseError { override name = "TransactionCallbackError"; - constructor({ cause }: { cause: cause }) { - super(undefined, { cause }); + constructor(message?: string | undefined, { cause }: { cause?: cause } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, TransactionCallbackError.prototype); } } -export class CopyFlushError< - cause extends Error | undefined = undefined, -> extends BaseError { - override name = "CopyFlushError"; - - constructor({ cause }: { cause?: cause } = {}) { - super(undefined, { cause }); - Object.setPrototypeOf(this, CopyFlushError.prototype); - } -} - export class DelayedInsertError< cause extends Error | undefined = undefined, > extends BaseError { override name = "DelayedInsertError"; - constructor({ cause }: { cause?: cause } = {}) { - super(undefined, { cause }); + constructor(message?: string | undefined, { cause }: { cause?: cause } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, DelayedInsertError.prototype); } } @@ -165,8 +154,8 @@ export class RawSqlError< > extends BaseError { override name = "RawSqlError"; - constructor({ cause }: { cause?: cause } = {}) { - super(undefined, { cause }); + constructor(message?: string | undefined, { cause }: { cause?: cause } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, RawSqlError.prototype); } } @@ -176,8 +165,8 @@ export class BigIntSerializationError< > extends BaseError { override name = "BigIntSerializationError"; - constructor({ cause }: { cause?: cause } = {}) { - super(undefined, { cause }); + constructor(message?: string | undefined, { cause }: { cause?: cause } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, BigIntSerializationError.prototype); } } @@ -190,8 +179,8 @@ export class ServerError< > extends BaseError { override name = "ServerError"; - constructor({ cause }: { cause?: cause } = {}) { - super(undefined, { cause }); + constructor(message?: string | undefined, { cause }: { cause?: cause } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, ServerError.prototype); } } @@ -204,8 +193,8 @@ export class IndexingFunctionError< > extends BaseError { override name = "IndexingFunctionError"; - constructor({ cause }: { cause?: cause } = {}) { - super(undefined, { cause }); + constructor(message?: string | undefined, { cause }: { cause?: cause } = {}) { + super(message, { cause }); Object.setPrototypeOf(this, IndexingFunctionError.prototype); } } @@ -228,7 +217,6 @@ export class MigrationError< cause extends Error | undefined = undefined, > extends BaseError { override name = "MigrationError"; - // TODO(kyle) exit code constructor(message?: string | undefined, { cause }: { cause?: cause } = {}) { super(message, { cause }); @@ -236,39 +224,23 @@ export class MigrationError< } } -// export const nonRetryableUserErrorNames = [ -// ShutdownError, -// BuildError, -// MigrationError, -// UniqueConstraintError, -// NotNullConstraintError, -// InvalidStoreAccessError, -// RecordNotFoundError, -// CheckConstraintError, -// InvalidStoreMethodError, -// UndefinedTableError, -// BigIntSerializationError, -// DelayedInsertError, -// RawSqlError, -// IndexingFunctionError, -// ].map((err) => err.name); - /** * Returns true if the error is derived from a logical error in user code. + * @dev `instanceof` is not used because it doesn't work with serialized errors + * from threads. */ export function isUserDerivedError(error: BaseError): boolean { - if (error instanceof BuildError) return true; - if (error instanceof ExecuteFileError) return true; - if (error instanceof IndexingDBError) return true; - if (error instanceof DelayedInsertError) return true; - if ( - error instanceof IndexingFunctionError && - error.cause instanceof BaseError === false - ) { - return true; - } - - if (error instanceof BaseError && error.cause) { + if (error.name === BuildError.name) return true; + if (error.name === ExecuteFileError.name) return true; + if (error.name === MigrationError.name) return true; + if (error.name === IndexingDBError.name) return true; + if (error.name === BigIntSerializationError.name) return true; + if (error.name === RawSqlError.name) return true; + if (error.name === DelayedInsertError.name) return true; + if (error.name === IndexingFunctionError.name) return true; + + if ("cause" in error) { + // @ts-ignore if (isUserDerivedError(error.cause)) return true; } return false; diff --git a/packages/core/src/runtime/isolated.ts b/packages/core/src/runtime/isolated.ts index 93bb69253..5d9a5ce7f 100644 --- a/packages/core/src/runtime/isolated.ts +++ b/packages/core/src/runtime/isolated.ts @@ -401,15 +401,16 @@ export async function runIsolated({ indexingCache.invalidate(); indexingCache.clear(); + common.logger.warn({ + msg: "Failed to index block range", + chain: chain.name, + chain_id: chain.id, + block_range: JSON.stringify(blockRange), + duration: indexStartClock(), + error: error as Error, + }); + if (error instanceof InvalidEventAccessError) { - common.logger.debug({ - msg: "Failed to index block range", - chain: chain.name, - chain_id: chain.id, - block_range: JSON.stringify(blockRange), - duration: indexStartClock(), - error, - }); events = await refetchHistoricalEvents({ common, indexingBuild, @@ -418,14 +419,6 @@ export async function runIsolated({ events, }); } - common.logger.warn({ - msg: "Failed to index block range", - chain: chain.name, - chain_id: chain.id, - block_range: JSON.stringify(blockRange), - duration: indexStartClock(), - error: error as Error, - }); throw error; } diff --git a/packages/core/src/runtime/multichain.ts b/packages/core/src/runtime/multichain.ts index fe1437fc0..abb9f1ed8 100644 --- a/packages/core/src/runtime/multichain.ts +++ b/packages/core/src/runtime/multichain.ts @@ -456,15 +456,16 @@ export async function runMultichain({ indexingCache.invalidate(); indexingCache.clear(); + common.logger.warn({ + msg: "Failed to index block range", + chain: chain.name, + chain_id: chain.id, + block_range: JSON.stringify(blockRange), + duration: indexStartClock(), + error: error as Error, + }); + if (error instanceof InvalidEventAccessError) { - common.logger.debug({ - msg: "Failed to index block range", - chain: chain.name, - chain_id: chain.id, - block_range: JSON.stringify(blockRange), - duration: indexStartClock(), - error, - }); events = await refetchHistoricalEvents({ common, indexingBuild, @@ -474,15 +475,6 @@ export async function runMultichain({ }); } - common.logger.warn({ - msg: "Failed to index block range", - chain: chain.name, - chain_id: chain.id, - block_range: JSON.stringify(blockRange), - duration: indexStartClock(), - error: error as Error, - }); - throw error; } }, diff --git a/packages/core/src/runtime/omnichain.ts b/packages/core/src/runtime/omnichain.ts index 9044e69e5..d35fb08ee 100644 --- a/packages/core/src/runtime/omnichain.ts +++ b/packages/core/src/runtime/omnichain.ts @@ -475,12 +475,13 @@ export async function runOmnichain({ indexingCache.invalidate(); indexingCache.clear(); + common.logger.warn({ + msg: "Failed to index block range", + duration: indexStartClock(), + error: error as Error, + }); + if (error instanceof InvalidEventAccessError) { - common.logger.debug({ - msg: "Failed to index block range", - duration: indexStartClock(), - error, - }); events = await refetchHistoricalEvents({ common, indexingBuild, @@ -489,11 +490,6 @@ export async function runOmnichain({ events, }); } - common.logger.warn({ - msg: "Failed to index block range", - duration: indexStartClock(), - error: error as Error, - }); throw error; } diff --git a/packages/core/src/server/error.ts b/packages/core/src/server/error.ts index 3241116e6..7a4848b91 100644 --- a/packages/core/src/server/error.ts +++ b/packages/core/src/server/error.ts @@ -6,7 +6,7 @@ import type { Context, HonoRequest } from "hono"; import { html } from "hono/html"; export const onError = async (_error: Error, c: Context, common: Common) => { - const error = new ServerError({ cause: _error }); + const error = new ServerError(undefined, { cause: _error }); // Find the filename where the error occurred const regex = /(\S+\.(?:js|ts|mjs|cjs)):\d+:\d+/;