Skip to content

CliError.InvalidValue.message doubles the "Expected: Expected ..." prefix for choice/schema-backed primitives #6312

Description

@Coly010

Title

CliError.InvalidValue.message doubles the "Expected: Expected ..." prefix for choice/schema-backed primitives

Body

Effect version

4.0.0-beta.93 (effect/unstable/cli)

Description

Any flag or argument backed by Primitive.choice (i.e. Flag.choice/Flag.choiceWithValue) or by the schema-backed primitives integer, float, boolean, date renders a doubled "Expected: Expected ..." prefix when given an invalid value, and also repeats the offending value twice in the same message.

Reproduction

import { Primitive, CliError } from "effect/unstable/cli"
import { Effect } from "effect"

const sizeChoice = Primitive.choice([
  ["small", "small"],
  ["medium", "medium"],
  ["large", "large"],
])

const program = Effect.gen(function* () {
  return yield* Effect.mapError(
    sizeChoice.parse("bogus"),
    (expected) =>
      new CliError.InvalidValue({ option: "size", value: "bogus", expected, kind: "flag" })
  )
})

Effect.runPromise(program.pipe(Effect.catch((error) => Effect.sync(() => {
  console.log("error.expected:", JSON.stringify(error.expected))
  console.log("error.message:", JSON.stringify(error.message))
  return error
}))))

This is exactly the path a real CLI hits through Flag.choice("size", [...]) used in a CommandParam.ts's parseFlag/parseArgument map a failed primitiveType.parse(arg) straight into CliError.InvalidValue's expected field.

Actual output

error.expected: "Expected \"small\" | \"medium\" | \"large\", got \"bogus\""
error.message: "Invalid value for flag --size: \"bogus\". Expected: Expected \"small\" | \"medium\" | \"large\", got \"bogus\""

Note both defects in error.message:

  1. "Expected: Expected ..." — doubled prefix.
  2. "bogus" appears twice — once as the flag's own value, once again via the primitive's , got "bogus" suffix.

Expected output

Something like:

Invalid value for flag --size: "bogus". Expected: "small" | "medium" | "large"

(a single "Expected" prefix, and the invalid value shown once)

Root cause

  • Primitive.choice fails with a raw string that already starts with "Expected " and already appends , got ${value}:

    return Effect.fail(`Expected ${validChoices}, got ${format(value)}`)

    The schema-backed primitives (boolean, float, integer, date, built via makeSchemaPrimitive) fail the same way, surfacing Schema's own decode-error message, which is also of the shape "Expected <X>, got <Y>".

  • Param.ts's parseFlag/parseArgument take that raw failure string verbatim and store it as CliError.InvalidValue's expected field:

    const value = yield* Effect.mapError(
      primitiveType.parse(arg),
      (error) =>
        new CliError.InvalidValue({
          option: name,
          value: arg,
          expected: error,
          kind: "flag"
        })
    )
  • CliError.InvalidValue's message getter unconditionally prepends its own "Expected: " label on top of expected, regardless of whether expected already starts with the word "Expected":

    override get message() {
      if (this.kind === "argument") {
        return `Invalid value for argument <${this.option}>: "${this.value}". Expected: ${this.expected}`
      }
      return `Invalid value for flag --${this.option}: "${this.value}". Expected: ${this.expected}`
    }

So any primitive whose failure text is a full sentence beginning with "Expected" (rather than a bare description of the expected shape, e.g. "small" | "medium" | "large") gets double-prefixed, and any primitive whose failure text also repeats the offending value (via a , got <value> suffix) shows that value twice.

Suggested fix

Pick one canonical contract for what a Primitive's failure message / InvalidValue.expected should contain, then make all primitives conform:

  • Option Aexpected should be a bare description of the accepted shape (e.g. "small" | "medium" | "large", no "Expected " prefix, no , got <value> suffix), and InvalidValue.message stays as the single source of the "Expected:"/"got" wording. This would mean adjusting Primitive.choice (drop the Expected ${...}, got ${...} wrapper, just fail with validChoices) and stripping/reshaping the Schema decode error before it reaches expected in the schema-backed primitives.
  • Option BInvalidValue.message detects and strips a leading "Expected " (and/or a trailing , got "<value>") from this.expected before composing the final sentence, so it degrades gracefully regardless of what a given Primitive reports.

Happy to open a PR for whichever direction maintainers prefer — we worked around this downstream in our own CLI (built on effect/unstable/cli) by post-processing the doubled substring in our error-formatting layer, but that's brittle against upstream wording changes and doesn't fix the duplicated-value wart.

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.0Used for issues, pull requests, etc. that are relevant for the `v3` branch targeting Effect v3.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions