Skip to main content

Documentation Index

Fetch the complete documentation index at: https://next-safe-env.dev/llms.txt

Use this file to discover all available pages before exploring further.

next-safe-env follows one rule for validation failures: fail at startup, never at runtime. When createEnv() is called at module load time, every field in the schema is validated before the function returns. If any field fails, all failures are collected first - the engine never short-circuits on the first error - and the process exits with a non-zero code after printing a complete list of every problem at once.

What the error output looks like

When validation fails, you see a structured message that lists every bad field along with what was expected, what was actually received, and a summary of how many server and client vars passed or failed:
[next-safe-env] Environment validation failed - 3 error(s):

  ✗ DATABASE_URL     - Required. Expected a valid URL. Got: "postgres-localhost"
  ✗ JWT_SECRET       - Too short. Must be ≥ 32 characters. Got length: 12
  ✗ SMTP_PORT        - Invalid port. Must be 1–65535. Got: "99999"

  Server vars:  2 valid, 3 invalid
  Client vars:  2 valid, 0 invalid

  Set the correct values in your .env file or deployment environment and restart.
Every failure line includes:
  • Field name - the exact key from your schema, left-padded for alignment
  • What was expected - the constraint that was not satisfied (e.g. valid URL, length >= 32, <= 65535)
  • What was received - the raw string value that was present, or "undefined" if the variable was missing
The summary block counts server and client vars independently, so you can see at a glance whether the failures are concentrated in one layer.

The EnvValidationError class

When validation fails, next-safe-env creates an instance of EnvValidationError and either passes it to your onValidationError handler or, if none is provided, calls console.error(err.format()) and process.exit(1) by default.
class EnvValidationError extends Error {
  readonly failures: ValidationFailure[]
  readonly stats: ValidationStats

  format(): string
  toJSON(): unknown
}

failures: ValidationFailure[]

An array containing one entry per failed field. Each entry has the shape:
type ValidationFailure = {
  field:    string  // the schema key name
  expected: string  // human-readable description of the expected value
  received: string  // the raw value that was present
  message:  string  // full message: `Expected ${expected}. Got: "${received}"`
}

stats: ValidationStats

An object with counts broken down by server and client:
type ValidationStats = {
  serverTotal:  number  // total server schema keys
  clientTotal:  number  // total client schema keys
  serverFailed: number  // server keys that failed validation
  clientFailed: number  // client keys that failed validation
}

format(): string

Returns the full pretty-printed multi-line string shown in the output above. This is what the default handler passes to console.error. Call it when you want to log the human-readable version.

toJSON(): unknown

Returns a plain JSON-serializable object:
{
  error:    'EnvValidationError',
  failures: ValidationFailure[],
  stats:    ValidationStats,
}
Use toJSON() when you need to send the error to a structured logging system, a remote error tracker, or any destination that expects JSON rather than a formatted string.

Custom error handlers with onValidationError

You can replace the default console.error + process.exit(1) behavior by providing an onValidationError function in the config:
import { createEnv, url, str } from 'next-safe-env'

export const env = createEnv({
  server: {
    DATABASE_URL: url(),
    JWT_SECRET:   str().min(32),
  },
  client: {},
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    JWT_SECRET:   process.env.JWT_SECRET,
  },
  onValidationError: (err) => {
    myLogger.fatal({ env_errors: err.toJSON() }, 'Environment validation failed')
    process.exit(1)
  },
})
The handler receives the EnvValidationError instance. You have access to err.failures, err.stats, err.format(), and err.toJSON() - use whichever form suits your logging infrastructure.
onValidationError must never return. The engine does not call process.exit() after invoking your handler. If the handler returns normally, execution continues with an invalid env object - every field that failed validation will be undefined or the wrong type, and your application will behave unpredictably. Always call process.exit() or throw an error inside the handler.
A handler for a structured logging setup might look like this:
import { createEnv, url, str, port } from 'next-safe-env'
import { logger } from './logger'

export const env = createEnv({
  server: {
    DATABASE_URL: url(),
    JWT_SECRET:   str().min(32),
    PORT:         port().default(3000),
  },
  client: {},
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    JWT_SECRET:   process.env.JWT_SECRET,
    PORT:         process.env.PORT,
  },
  onValidationError: (err) => {
    logger.fatal(
      {
        failureCount: err.failures.length,
        failures:     err.failures,
        stats:        err.stats,
      },
      '[next-safe-env] Startup aborted: invalid environment configuration',
    )
    process.exit(1)
  },
})
This sends the structured failures array and stats object to your log aggregator while preserving the non-zero exit code that signals a failed deployment to your process manager or container orchestrator.
The onValidationError option accepts a function typed as (error: ValidationErrorShape) => never. The never return type is enforced by TypeScript - if your handler has a code path that could return, the compiler will error.