From 9014661f70a15d29e3168bbe25e32528a6229626 Mon Sep 17 00:00:00 2001 From: James Martinez Date: Thu, 5 Feb 2026 16:26:12 -0600 Subject: [PATCH] Improve database loading/connection error logs --- packages/openworkflow/core/error.test.ts | 20 +++++++++++++++- packages/openworkflow/core/error.ts | 11 +++++++++ .../openworkflow/postgres/backend.test.ts | 10 +++++++- packages/openworkflow/postgres/backend.ts | 23 +++++++++++++------ packages/openworkflow/sqlite/backend.test.ts | 16 ++++++++++++- packages/openworkflow/sqlite/backend.ts | 19 +++++++++++---- 6 files changed, 84 insertions(+), 15 deletions(-) diff --git a/packages/openworkflow/core/error.test.ts b/packages/openworkflow/core/error.test.ts index ab124912..85dd19ea 100644 --- a/packages/openworkflow/core/error.test.ts +++ b/packages/openworkflow/core/error.test.ts @@ -1,4 +1,4 @@ -import { serializeError } from "./error.js"; +import { serializeError, wrapError } from "./error.js"; import { describe, expect, test } from "vitest"; describe("serializeError", () => { @@ -76,3 +76,21 @@ describe("serializeError", () => { expect(result.message).toBe("[object Object]"); }); }); + +describe("wrapError", () => { + test("wraps errors with serialized cause", () => { + const original = new Error("boom"); + const wrapped = wrapError("Top-level", original); + + expect(original.message).toBe("boom"); + expect(wrapped.message).toBe("Top-level: boom"); + expect(wrapped.cause).toBe(original); + }); + + test("wraps string errors with serialized cause", () => { + const wrapped = wrapError("Top-level", "boom"); + + expect(wrapped.message).toBe("Top-level: boom"); + expect(wrapped.cause).toBe("boom"); + }); +}); diff --git a/packages/openworkflow/core/error.ts b/packages/openworkflow/core/error.ts index 6e06052f..adfe1ca8 100644 --- a/packages/openworkflow/core/error.ts +++ b/packages/openworkflow/core/error.ts @@ -27,3 +27,14 @@ export function serializeError(error: unknown): SerializedError { message: String(error), }; } + +/** + * Wrap an error with a clearer message while preserving the original cause. + * @param message - The message to use for the new error + * @param error - The original error + * @returns A new error with the original error as its cause + */ +export function wrapError(message: string, error: unknown): Error { + const { message: wrappedMessage } = serializeError(error); + return new Error(`${message}: ${wrappedMessage}`, { cause: error }); +} diff --git a/packages/openworkflow/postgres/backend.test.ts b/packages/openworkflow/postgres/backend.test.ts index 7c744982..3b2deb48 100644 --- a/packages/openworkflow/postgres/backend.test.ts +++ b/packages/openworkflow/postgres/backend.test.ts @@ -3,7 +3,7 @@ import { BackendPostgres } from "./backend.js"; import { DEFAULT_POSTGRES_URL } from "./postgres.js"; import assert from "node:assert"; import { randomUUID } from "node:crypto"; -import { test } from "vitest"; +import { describe, expect, test } from "vitest"; test("it is a test file (workaround for sonarjs/no-empty-test-file linter)", () => { assert.ok(true); @@ -19,3 +19,11 @@ testBackend({ await backend.stop(); }, }); + +describe("BackendPostgres.connect errors", () => { + test("returns a helpful error for invalid connection URLs", async () => { + await expect(BackendPostgres.connect("not-a-valid-url")).rejects.toThrow( + /Postgres backend failed to connect.*postgresql:\/\/user:pass@host:port\/db.*:/, + ); + }); +}); diff --git a/packages/openworkflow/postgres/backend.ts b/packages/openworkflow/postgres/backend.ts index f61cfe7a..751880ce 100644 --- a/packages/openworkflow/postgres/backend.ts +++ b/packages/openworkflow/postgres/backend.ts @@ -17,6 +17,7 @@ import { CompleteWorkflowRunParams, SleepWorkflowRunParams, } from "../backend.js"; +import { wrapError } from "../core/error.js"; import { JsonValue } from "../core/json.js"; import { DEFAULT_RETRY_POLICY } from "../core/retry.js"; import { StepAttempt } from "../core/step.js"; @@ -55,6 +56,7 @@ export class BackendPostgres implements Backend { * @param url - Postgres connection URL * @param options - Backend options * @returns A connected backend instance + * @throws {Error} Error connecting to the Postgres database */ static async connect( url: string, @@ -66,14 +68,21 @@ export class BackendPostgres implements Backend { ...options, }; - if (runMigrations) { - const pgForMigrate = newPostgresMaxOne(url); - await migrate(pgForMigrate, DEFAULT_SCHEMA); - await pgForMigrate.end(); - } + try { + if (runMigrations) { + const pgForMigrate = newPostgresMaxOne(url); + await migrate(pgForMigrate, DEFAULT_SCHEMA); + await pgForMigrate.end(); + } - const pg = newPostgres(url); - return new BackendPostgres(pg, namespaceId); + const pg = newPostgres(url); + return new BackendPostgres(pg, namespaceId); + } catch (error) { + throw wrapError( + 'Postgres backend failed to connect. Check the connection URL (e.g. "postgresql://user:pass@host:port/db").', + error, + ); + } } async stop(): Promise { diff --git a/packages/openworkflow/sqlite/backend.test.ts b/packages/openworkflow/sqlite/backend.test.ts index 2e8797dc..33c478c8 100644 --- a/packages/openworkflow/sqlite/backend.test.ts +++ b/packages/openworkflow/sqlite/backend.test.ts @@ -5,7 +5,7 @@ import { randomUUID } from "node:crypto"; import { unlinkSync, existsSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; -import { test, describe, afterAll } from "vitest"; +import { test, describe, afterAll, expect } from "vitest"; test("it is a test file (workaround for sonarjs/no-empty-test-file linter)", () => { assert.ok(true); @@ -60,3 +60,17 @@ describe("BackendSqlite (file-based)", () => { }, }); }); + +describe("BackendSqlite.connect errors", () => { + test("returns a helpful error for invalid database paths", () => { + const badPath = path.join( + tmpdir(), + `openworkflow-missing-${randomUUID()}`, + "backend.db", + ); + + expect(() => BackendSqlite.connect(badPath)).toThrow( + /SQLite backend failed to open database.*valid and writable.*:/, + ); + }); +}); diff --git a/packages/openworkflow/sqlite/backend.ts b/packages/openworkflow/sqlite/backend.ts index ba875cb2..c723fe5f 100644 --- a/packages/openworkflow/sqlite/backend.ts +++ b/packages/openworkflow/sqlite/backend.ts @@ -17,6 +17,7 @@ import { CompleteWorkflowRunParams, SleepWorkflowRunParams, } from "../backend.js"; +import { wrapError } from "../core/error.js"; import { JsonValue } from "../core/json.js"; import { DEFAULT_RETRY_POLICY } from "../core/retry.js"; import { StepAttempt } from "../core/step.js"; @@ -60,6 +61,7 @@ export class BackendSqlite implements Backend { * @param path - Database path * @param options - Backend options * @returns A connected backend instance + * @throws {Error} Error connecting to the SQLite database */ static connect(path: string, options?: BackendSqliteOptions): BackendSqlite { const { namespaceId, runMigrations } = { @@ -68,13 +70,20 @@ export class BackendSqlite implements Backend { ...options, }; - const db = newDatabase(path); + try { + const db = newDatabase(path); - if (runMigrations) { - migrate(db); - } + if (runMigrations) { + migrate(db); + } - return new BackendSqlite(db, namespaceId); + return new BackendSqlite(db, namespaceId); + } catch (error) { + throw wrapError( + "SQLite backend failed to open database. Check the path is valid and writable.", + error, + ); + } } // eslint-disable-next-line @typescript-eslint/require-await