Output & Errors
When you run a command with Command.run, two kinds of text reach the terminal
that you never wrote by hand: help output (printed for --help, --version,
or when parsing fails) and error messages (printed when a flag is missing, a
value is invalid, an unknown subcommand is used, and so on). Both are produced by
a single service, CliOutput.Formatter, which the runner reads from context.
This page covers how to override that formatter (most commonly to turn colors
off in CI), the HelpDoc data model the formatter consumes, and the structured
CliError types the parser raises — which you can pattern-match when a command is
embedded inside a larger program.
All three modules import from effect/unstable/cli:
import { CliError, CliOutput, HelpDoc } from "effect/unstable/cli"The common case: disable colors in CI
Section titled “The common case: disable colors in CI”By default the formatter auto-detects color support from process.stdout.isTTY
and the NO_COLOR environment variable. In CI you often want to force plain text
so logs stay readable. Build a no-color formatter with
CliOutput.defaultFormatter({ colors: false }) and provide it through
CliOutput.layer:
import { NodeRuntime, NodeServices } from "@effect/platform-node"import { Console, Effect } from "effect"import { Command, CliOutput, Flag } from "effect/unstable/cli"
const greet = Command.make("greet", { name: Flag.string("name")}, (config) => Console.log(`Hello, ${config.name}!`))
// A formatter that never emits ANSI color codes.const NoColor = CliOutput.layer( CliOutput.defaultFormatter({ colors: false }))
Command.run(greet, { version: "1.0.0" }).pipe( // Override the formatter the runner pulls from context. Effect.provide(NoColor), Effect.provide(NodeServices.layer), NodeRuntime.runMain)Now greet --help, greet --version, and any parse error render as plain text.
Because CliOutput.Formatter is a Context.Reference with a default value, you
do not have to provide it — the layer simply replaces the default.
A fully custom formatter is just an object implementing the Formatter
interface — useful for machine-readable output such as JSON:
import { CliOutput } from "effect/unstable/cli"
const jsonFormatter: CliOutput.Formatter = { formatHelpDoc: (doc) => JSON.stringify(doc, null, 2), formatCliError: (error) => JSON.stringify({ error: error.message }), formatError: (error) => JSON.stringify({ type: "error", message: error.message }), formatVersion: (name, version) => JSON.stringify({ name, version }), formatErrors: (errors) => JSON.stringify(errors.map((e) => e.message))}
const JsonLayer = CliOutput.layer(jsonFormatter)CliOutput reference
Section titled “CliOutput reference”The CliOutput module is small: an interface, a default implementation, and a
layer to install it.
CliOutput.Formatter
Section titled “CliOutput.Formatter”The service interface describing how help, usage, version, and errors render. It
is exposed as a Context.Reference, so it always has a default and is accessed
with yield* CliOutput.Formatter. The interface has five methods:
| Method | Signature | Renders |
|---|---|---|
formatHelpDoc | (doc: HelpDoc) => string | the full help screen for --help |
formatVersion | (name: string, version: string) => string | the --version line |
formatError | (error: CliError) => string | a single error block |
formatErrors | (errors: ReadonlyArray<CliError>) => string | several errors, grouped by tag |
formatCliError | (error: CliError) => string | the bare error message (no styling) |
import { Effect } from "effect"import { CliOutput } from "effect/unstable/cli"
const program = Effect.gen(function*() { // Read the formatter the runner would use from context. const formatter = yield* CliOutput.Formatter return formatter.formatVersion("my-cli", "2.1.0")})
// With the default (no-color) formatter:// => "my-cli v2.1.0"CliOutput.defaultFormatter
Section titled “CliOutput.defaultFormatter”CliOutput.defaultFormatter(options?: { colors?: boolean }) builds the standard
formatter. With no argument it auto-detects color support; pass colors to force
it on or off. This is the value behind the Formatter reference’s default.
import { CliError, CliOutput } from "effect/unstable/cli"
const plain = CliOutput.defaultFormatter({ colors: false })
plain.formatVersion("my-tool", "1.2.3")// => "my-tool v1.2.3"
plain.formatError( new CliError.MissingOption({ option: "name" }))// => "\nERROR\n Missing required flag: --name"
const colored = CliOutput.defaultFormatter({ colors: true })colored.formatVersion("my-tool", "1.2.3")// => "\x1b[1mmy-tool\x1b[0m \x1b[2mv\x1b[0m\x1b[1m1.2.3\x1b[0m"The help renderer produces sections in a fixed order — DESCRIPTION, USAGE,
ARGUMENTS, FLAGS, GLOBAL FLAGS, SUBCOMMANDS, and EXAMPLES — measuring
visible width after stripping ANSI codes so colored and plain output stay aligned.
CliOutput.layer
Section titled “CliOutput.layer”CliOutput.layer(formatter: Formatter): Layer.Layer<never> provides a formatter,
replacing the default. Provide it to Command.run to change how the whole CLI
renders.
import { Effect } from "effect"import { Command, CliOutput, Flag } from "effect/unstable/cli"
const cmd = Command.make("hi", { name: Flag.string("name") }, () => Effect.void)
const run = Command.run(cmd, { version: "1.0.0" }).pipe( Effect.provide(CliOutput.layer(CliOutput.defaultFormatter({ colors: false }))))// => `--help`/`--version`/errors now render without ANSI colorsHelpDoc reference
Section titled “HelpDoc reference”HelpDoc is the format-agnostic description of a command’s help. The runner
builds a HelpDoc from your command tree and hands it to
formatter.formatHelpDoc; the formatter decides layout, styling, and alignment.
You only touch these types when writing a custom formatter — building or
inspecting a doc rather than rendering it yourself.
HelpDoc.HelpDoc
Section titled “HelpDoc.HelpDoc”The top-level shape. description, usage, and flags are always present;
args, globalFlags, subcommands, and examples are optional and omitted when
empty. annotations is a Context.Context<never> carrying custom command
metadata.
import { Context, Option } from "effect"import type { HelpDoc } from "effect/unstable/cli"
const doc: HelpDoc.HelpDoc = { description: "Deploy your application", usage: "myapp deploy <target>", annotations: Context.empty(), flags: [], args: [ { name: "target", type: "string", description: Option.some("Where to deploy"), required: true, variadic: false } ]}// => consumed by formatter.formatHelpDoc(doc)HelpDoc.FlagDoc
Section titled “HelpDoc.FlagDoc”Describes one flag: its primary name (without the --), aliases, a type
label like "string" / "boolean" / "integer", an Option<string>
description, and whether it is required.
import { Option } from "effect"import type { HelpDoc } from "effect/unstable/cli"
const verbose: HelpDoc.FlagDoc = { name: "verbose", aliases: ["-v"], type: "boolean", description: Option.some("Enable verbose output"), required: false}// => renders as: --verbose, -v Enable verbose outputHelpDoc.ArgDoc
Section titled “HelpDoc.ArgDoc”Describes one positional argument. Like FlagDoc but adds variadic (accepts
multiple values) and has no aliases.
import { Option } from "effect"import type { HelpDoc } from "effect/unstable/cli"
const files: HelpDoc.ArgDoc = { name: "files", type: "file", description: Option.some("Files to process"), required: false, variadic: true}// => renders the name as "files..." in the ARGUMENTS tableHelpDoc.SubcommandDoc
Section titled “HelpDoc.SubcommandDoc”Describes a single subcommand row: name, an optional alias, an optional
shortDescription (preferred in listings when present), and a full description.
import type { HelpDoc } from "effect/unstable/cli"
const deploy: HelpDoc.SubcommandDoc = { name: "deploy", alias: "d", shortDescription: "Deploy app", description: "Deploy the application to the cloud"}// => renders as: deploy, d Deploy appHelpDoc.SubcommandGroupDoc
Section titled “HelpDoc.SubcommandGroupDoc”Groups subcommands under an optional heading. group: undefined is the default
ungrouped SUBCOMMANDS section; a string becomes a named heading. commands is a
non-empty array of SubcommandDoc.
import type { HelpDoc } from "effect/unstable/cli"
const group: HelpDoc.SubcommandGroupDoc = { group: undefined, // ungrouped → "SUBCOMMANDS" heading commands: [ { name: "deploy", alias: "d", shortDescription: undefined, description: "Deploy" } ]}HelpDoc.ExampleDoc
Section titled “HelpDoc.ExampleDoc”A concrete invocation example: a command string and an optional description
rendered as a # comment above it in the EXAMPLES section.
import type { HelpDoc } from "effect/unstable/cli"
const example: HelpDoc.ExampleDoc = { command: "myapp deploy production --force", description: "Force a production deploy"}// => renders as:// # Force a production deploy// myapp deploy production --forceCliError reference
Section titled “CliError reference”When parsing fails, the runner accumulates CliError values and prints them
through the formatter. Every variant is a Schema.ErrorClass, so it carries
structured fields (not just a string) and a _tag you can match on, and each
computes a human-readable message getter.
You rarely construct these yourself; the parser does. But because they are tagged
errors, you can catch them with Effect.catchTag
when you embed a command’s effect inside a larger program rather than letting
Command.run format and exit.
CliError.UnrecognizedOption
Section titled “CliError.UnrecognizedOption”An unknown flag was passed. Fields: option, optional command (the command path
it appeared in), and suggestions (close matches for a “Did you mean?” hint).
import { CliError } from "effect/unstable/cli"
new CliError.UnrecognizedOption({ option: "--forc", command: ["deploy"], suggestions: ["--force"]}).message// => "Unrecognized flag: --forc in command deploy//// Did you mean this?// --force"CliError.DuplicateOption
Section titled “CliError.DuplicateOption”A flag name is defined on both a parent command and one of its subcommands. The
parent always claims the flag, so this is reported as a definition problem. Fields:
option, parentCommand, childCommand.
import { CliError } from "effect/unstable/cli"
new CliError.DuplicateOption({ option: "--verbose", parentCommand: "myapp", childCommand: "deploy"}).message// => 'Duplicate flag name "--verbose" in parent command "myapp" and subcommand// "deploy". Parent will always claim this flag (Mode A semantics)...'CliError.MissingOption
Section titled “CliError.MissingOption”A required flag was not supplied. Field: option (the name, without --).
import { CliError } from "effect/unstable/cli"
new CliError.MissingOption({ option: "api-key" }).message// => "Missing required flag: --api-key"CliError.MissingArgument
Section titled “CliError.MissingArgument”A required positional argument was not supplied. Field: argument.
import { CliError } from "effect/unstable/cli"
new CliError.MissingArgument({ argument: "target" }).message// => "Missing required argument: target"CliError.InvalidValue
Section titled “CliError.InvalidValue”A flag or argument value failed validation (e.g. a non-integer passed to an
integer flag). Fields: option, value, expected, and kind
("flag" | "argument"), which selects the message phrasing.
import { CliError } from "effect/unstable/cli"
new CliError.InvalidValue({ option: "port", value: "abc", expected: "integer between 1 and 65535", kind: "flag"}).message// => 'Invalid value for flag --port: "abc". Expected: integer between 1 and 65535'
new CliError.InvalidValue({ option: "count", value: "abc", expected: "integer", kind: "argument"}).message// => 'Invalid value for argument <count>: "abc". Expected: integer'CliError.UnknownSubcommand
Section titled “CliError.UnknownSubcommand”A subcommand name did not match any defined child. Fields: subcommand, optional
parent (the parent command path), and suggestions.
import { CliError } from "effect/unstable/cli"
new CliError.UnknownSubcommand({ subcommand: "deplyo", parent: ["myapp"], suggestions: ["deploy", "destroy"]}).message// => 'Unknown subcommand "deplyo" for "myapp"//// Did you mean this?// deploy// destroy'CliError.UserError
Section titled “CliError.UserError”Wraps a failure thrown from inside a command handler so it travels in the CLI
error channel. Field: cause (the original defect/error). Unlike the parse errors
above, it has no custom message getter — inspect cause directly.
import { CliError } from "effect/unstable/cli"
const err = new CliError.UserError({ cause: new Error("Database connection failed")})err._tag // => "UserError"err.cause // => Error: Database connection failedCliError.ShowHelp
Section titled “CliError.ShowHelp”A control-flow signal, not a parse failure. It is raised for explicit --help
requests and for parse failures that should be shown alongside help. Fields:
commandPath (which command’s help to render) and errors (a possibly-empty list
of the non-help errors below). It exits with code 0 when errors is empty and
1 otherwise.
import { CliError } from "effect/unstable/cli"
const help = new CliError.ShowHelp({ commandPath: ["myapp", "deploy"], errors: [new CliError.MissingOption({ option: "target" })]})help.message // => "Help requested"help.errors.length // => 1 (so it exits with code 1)CliError.NonShowHelpErrors
Section titled “CliError.NonShowHelpErrors”The Schema.Union of every concrete error except ShowHelp. ShowHelp.errors
is typed as an array of these, so a help screen can carry the underlying failures
without nesting another help-control value. The corresponding type alias is
CliError.NonShowHelpErrors.
import { CliError } from "effect/unstable/cli"
// Decode an unknown value into one of the concrete (non-ShowHelp) errors.CliError.NonShowHelpErrors// => Schema.Union of UnrecognizedOption | DuplicateOption | MissingOption// | MissingArgument | InvalidValue | UnknownSubcommand | UserErrorCliError.CliError
Section titled “CliError.CliError”The union of every variant: UnrecognizedOption | DuplicateOption | MissingOption | MissingArgument | InvalidValue | UnknownSubcommand | ShowHelp | UserError. This
is the error type that appears in Command.run’s signature.
CliError.isCliError
Section titled “CliError.isCliError”A type guard, (u: unknown) => u is CliError, that checks for the internal CLI
error brand. Handy at the boundary of a larger program where you have caught an
unknown and want to distinguish CLI failures from other defects.
import { CliError } from "effect/unstable/cli"
const err = new CliError.MissingOption({ option: "name" })
CliError.isCliError(err) // => trueCliError.isCliError(new Error("boom")) // => falseCatching CLI errors in a larger program
Section titled “Catching CLI errors in a larger program”Command.run formats these errors and sets the process exit code for you. But if
you embed a command’s effect inside a bigger pipeline, the CliError surfaces in
the typed error channel and you can recover from specific variants with
Effect.catchTag:
import { Console, Effect } from "effect"import { Command, CliError, Flag } from "effect/unstable/cli"
const cmd = Command.make("deploy", { target: Flag.string("target") }, () => Console.log("deploying..."))
const program = Command.run(cmd, { version: "1.0.0" }).pipe( // Recover from a missing required flag instead of letting it exit. Effect.catchTag("MissingOption", (error) => Console.error(`You forgot --${error.option}`) ))See Catching Errors for the full set of
recovery combinators (catch, catchTag, catchTags, catchReason).