Debugging Environment Variable Errors

2026-06-19·Ctrotech·
guidedebugging

Debugging Environment Variable Errors

CtroEnv groups errors into four codes. Each tells you exactly what went wrong and where.

Error Codes

CodeMeaningWhen
missing_requiredVariable not found in sourceRequired var with no .optional() or .default()
type_mismatchWrong JavaScript typeString for number, boolean for URL, etc.
invalid_valueFailed refinementURL doesn't parse, port out of range
validation_failedCustom .validate() rejectedYour validation function returned an error

Missing Required

The most common error. A variable is required but not set in any source:

✗ DATABASE_URL: Missing required environment variable

Checks:

  • Is the variable in your .env file?
  • Does your source include the right file? loadEnv() reads .env → .env.{NODE_ENV} → .env.local
  • Did you use .optional() or .default() for variables that might not exist?
// Will throw if not set
DATABASE_URL: string().url()

// Will not throw if not set
DATABASE_URL: string().url().optional()
CACHE_TTL: number().default(3000)

Type Mismatch

The value exists but has the wrong JavaScript type:

✗ PORT: Expected a number, received string "hello"

CtroEnv accepts string coercion for numbers and booleans, but the string must be parseable:

// ✅ Accepted as number 3000
PORT: number().port()
// With PORT="3000" → 3000

// ❌ "abc" can't be coerced
// With PORT="abc" → type_mismatch

For number() specifically, the string must be a plain decimal:

✅ "3000"    → 3000
✅ "3.14"    → 3.14
✅ "-5"      → -5
❌ "0xFF"    → type_mismatch (hex)
❌ "1e2"     → type_mismatch (scientific)
❌ "   "     → type_mismatch (whitespace only)

Invalid Value

The value is the right type but fails a refinement:

✗ DATABASE_URL: Invalid URL
✗ PORT: Expected a port number (1-65535), received 70000
✗ JWT_SECRET: Must be at least 32 characters, received "abc"

Common causes:

RefinementCommon mistake
.url()Missing protocol: example.com instead of https://example.com
.url()file:// protocol is rejected
.port()Port 0 (reserved) or > 65535
.int()Float value like 3.14
.positive()Zero or negative number
.min(n)String shorter than n characters
.max(n)String longer than n characters
.email()Missing @ or TLD

Validation Failed

Your custom .validate() function returned an error string:

API_KEY: string().validate((value) => {
  if (!value.startsWith("sk_")) {
    return "API_KEY must start with 'sk_'"
  }
})
✗ API_KEY: API_KEY must start with 'sk_'

The validation function runs after type checking and refinements. The value is guaranteed to be the correct type when your function receives it.

Multiple Errors

CtroEnv collects all errors before throwing. You get every problem at once instead of a frustrating fix-one-find-another loop:

try {
  defineEnv(schema)
} catch (e) {
  if (e instanceof CtroEnvError) {
    // e.errors contains ALL problems
    console.error(formatErrors(e.errors))
    // Grouped output with all errors listed
  }
}

Using formatErrors for Better Output

formatErrors() colors and groups errors by type:

import { formatErrors } from "@ctroenv/core"

try {
  defineEnv(schema)
} catch (e) {
  if (e instanceof CtroEnvError) {
    process.stderr.write(formatErrors(e.errors))
  }
}

Output:

Missing Required Variables:
  DATABASE_URL  — Primary database connection

Invalid Values:
  PORT          — Expected a port number (1-65535), received 0

Descriptions from .describe() are included, making it clear which variable has which role.