Skip to main content
Lion uses Effect to model computation, which means errors are values tracked in the type system rather than exceptions thrown at runtime. When something goes wrong during evaluation — a bad argument, an unresolvable function call, or a malformed expression — Lion fails the Effect with a typed error that you can match, recover from, or propagate without a try/catch anywhere in sight.

The EvaluateResult type

The return type of run and the internal evaluate function is EvaluateResult:
import type { Effect } from "effect";
import type { ParseError } from "effect/ParseResult";
import type { ArgumentMismatchError, InvalidFunctionCallError } from "@lionlang/core/errors/evaluation";
import type { LionEnvironmentService } from "@lionlang/core/services/evaluation";

type EvaluateResult = Effect.Effect<
  unknown,
  ParseError | ArgumentMismatchError | InvalidFunctionCallError,
  LionEnvironmentService
>;
EvaluateResult is Effect<unknown, ParseError | ArgumentMismatchError | InvalidFunctionCallError, LionEnvironmentService>. The three type parameters are the success value, the union of possible errors, and the required service. run provides LionEnvironmentService automatically from the environment you pass, so by the time you reach Effect.runPromise the requirement is fully satisfied.

Error types

ParseError

Produced by Effect Schema when the input to run does not match LionExpressionSchema. This happens before any evaluation occurs — the input is not a valid Lion expression at all. A ParseError carries a structured description of what was expected versus what was received.

ArgumentMismatchError

Raised when a stdlib function receives arguments of the wrong type. For example, passing a string where a number is required in number/add. The _tag field is "ArgumentMismatchError".

InvalidFunctionCallError

Raised when an array expression’s first element does not resolve to a callable value. If Lion evaluates ["notAFunction", 1, 2] and notAFunction is not a function in the environment, evaluation fails with this error. The expression field contains the original Lion expression for debugging. The _tag field is "InvalidFunctionCallError".

Handling all errors with Effect.catchAll

Effect.catchAll intercepts any failure in the Effect and lets you return a fallback value or a new Effect:
import { Effect } from "effect";
import { run } from "@lionlang/core/evaluation/evaluate";
import { stdlib } from "@lionlang/core/modules";

const program = run(["number/add", "not-a-number", 2], stdlib);

const result = await Effect.runPromise(
  Effect.catchAll(program, (error) =>
    Effect.succeed(`Error: ${error._tag}`)
  )
);

// => "Error: ArgumentMismatchError"
The error parameter is typed as ParseError | ArgumentMismatchError | InvalidFunctionCallError, so your editor will offer completions for all available fields.

Handling specific error types with Effect.catchTag

When you only want to recover from one kind of error, use Effect.catchTag. Tags correspond to the class names: "ArgumentMismatchError", "InvalidFunctionCallError", and "ParseError".
import { Effect } from "effect";
import { run } from "@lionlang/core/evaluation/evaluate";
import { stdlib } from "@lionlang/core/modules";

const program = run(["object/call-method", "notAnObject", "toString"], stdlib);

const result = await Effect.runPromise(
  program.pipe(
    Effect.catchTag("InvalidFunctionCallError", (error) => {
      console.error("Head of expression did not resolve to a function:", error.expression);
      return Effect.succeed(null);
    }),
    Effect.catchTag("ArgumentMismatchError", (error) => {
      console.error("Wrong argument types:", error._tag);
      return Effect.succeed(null);
    })
  )
);
Errors not matched by a catchTag remain in the failure channel and propagate to the next handler or to runPromise, which will reject the returned Promise.

Surfacing parse errors separately

ParseError arrives from Effect Schema’s Schema.decodeUnknown call inside run. You can intercept it before the other errors to give users a distinct message when the input is structurally invalid:
import { Effect } from "effect";
import { run } from "@lionlang/core/evaluation/evaluate";
import { stdlib } from "@lionlang/core/modules";

const invalidExpression = { notALionExpression: true };

const result = await Effect.runPromise(
  run(invalidExpression, stdlib).pipe(
    Effect.catchTag("ParseError", (_error) =>
      Effect.succeed("Input is not a valid Lion expression")
    ),
    Effect.catchAll((_error) =>
      Effect.succeed("Evaluation failed")
    )
  )
);

// => "Input is not a valid Lion expression"
ParseError from Effect Schema does not have a _tag property matching "ParseError" in the same way as the Lion-specific errors. Catch it with Effect.catchTag("ParseError", ...) or use Effect.catchAll to handle all errors uniformly.

Letting errors propagate as rejected Promises

If you prefer to handle errors in a traditional try/catch, do not add any catchAll or catchTagEffect.runPromise will reject the Promise when the Effect fails:
import { Effect } from "effect";
import { run } from "@lionlang/core/evaluation/evaluate";
import { stdlib } from "@lionlang/core/modules";

try {
  const result = await Effect.runPromise(
    run(["number/add", "not-a-number", 2], stdlib)
  );
} catch (error) {
  // error is the ArgumentMismatchError instance
  console.error(error);
}
This works but loses the compile-time exhaustiveness guarantee that Effect provides. Prefer the typed Effect combinators when possible.