Debugging Environment Variable Errors
Debugging Environment Variable Errors
CtroEnv groups errors into four codes. Each tells you exactly what went wrong and where.
Error Codes
| Code | Meaning | When |
|---|---|---|
missing_required | Variable not found in source | Required var with no .optional() or .default() |
type_mismatch | Wrong JavaScript type | String for number, boolean for URL, etc. |
invalid_value | Failed refinement | URL doesn't parse, port out of range |
validation_failed | Custom .validate() rejected | Your 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
.envfile? - 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:
| Refinement | Common 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.