unionlabs/union · error · Error

Config file not found. Ensure config.json exists.

Error message

Config file not found. Ensure config.json exists.

What it means

sentinel2's loadConfig runs inside Effect.tryPromise: it checks Fs.existsSync(configPath) and throws this error when the config file is absent. The surrounding catch wraps ANY throw into FilesystemError('Config file is invalid.'), so operators usually see the misleading 'invalid' message and must dig into the cause chain to find the real 'not found' error.

Source

Thrown at sentinel2/src/helpers.ts:76

  betterstack_api_key: string
  trigger_betterstack: boolean
  dbPath: string
  isLocal: boolean
}

class FilesystemError extends Data.TaggedError("FilesystemError")<{
  message: string
  cause: unknown
}> {}

export class Config extends Context.Tag("Config")<Config, { readonly config: ConfigFile }>() {}

export function loadConfig(configPath: string) {
  return Effect.tryPromise({
    // biome-ignore lint/suspicious/useAwait: <explanation>
    try: async () => {
      if (!Fs.existsSync(configPath)) {
        throw new Error("Config file not found. Ensure config.json exists.")
      }
      const rawData = Fs.readFileSync(configPath, "utf-8")
      const config: ConfigFile = JSON.parse(rawData)

      return config
    },
    catch: error =>
      new FilesystemError({
        message: "Config file is invalid.",
        cause: error,
      }),
  })
}

export function hexToUtf8(hex: string): string {
  // strip optional 0x
  const clean = hex.startsWith("0x") ? hex.slice(2) : hex
  // build a Buffer from hex, then decode as UTF‑8

View on GitHub (pinned to 031785bb6d)

Solutions

  1. Create config.json at the exact path passed to loadConfig (or run from the directory that contains it)
  2. Pass an absolute config path (env var or CLI arg) instead of relying on cwd
  3. If you saw 'Config file is invalid.', inspect the FilesystemError's cause — it is often this not-found error, not a JSON problem
  4. Fix the catch to distinguish missing file from parse failure so the message matches reality

Example fix

// before
catch: error =>
  new FilesystemError({
    message: "Config file is invalid.",
    cause: error,
  }),

// after
catch: error =>
  new FilesystemError({
    message: Fs.existsSync(configPath)
      ? "Config file is invalid."
      : `Config file not found at ${configPath}.`,
    cause: error,
  }),
Defensive patterns

Strategy: validation

Validate before calling

import Fs from "node:fs"

const configPath = path.resolve(process.cwd(), "config.json")
if (!Fs.existsSync(configPath)) {
  console.error(`Missing ${configPath} — copy config.example.json and edit it`)
  process.exit(1)
}
await Effect.runPromise(loadConfig(configPath))

Try / catch

import { Effect } from "effect"

const program = loadConfig(configPath).pipe(
  Effect.catchTag("FilesystemError", e => {
    // e.cause holds the real error: missing file vs JSON.parse failure
    console.error(e.message, e.cause)
    return Effect.fail(e)
  }),
)

Prevention

When it happens

Trigger: Running the sentinel binary/bun process from a working directory where config.json does not exist; passing a wrong config path argument; containers/CI where the config volume mount is missing.

Common situations: Relative configPath resolved against an unexpected cwd; config.json gitignored and never deployed; fresh clones running before creating the config.

Related errors


AI-assisted analysis of unionlabs/union@031785bb6d (2026-08-16). Data as JSON: /api/errors/07d56f27c7f1c575. Report an issue: GitHub.