Skip to content
LogoLogo

Expression-Oriented Control Flow

Use conditions, switches, blocks, and loops as values

VibeLang makes common control-flow forms usable as expressions. New syntax follows Zig where TypeScript does not already provide a suitable form.

If Expressions

An if can produce a value:

const access = if (user.isAdmin) "all" else "limited"

Each reachable branch must produce a compatible type. A block returns its final expression:

const price = if (customer.isMember) {
  const discount = calculateDiscount(customer)
  subtotal * (1 - discount)
} else {
  subtotal
}

Statement-form TypeScript if remains valid.

Switch Expressions

A switch can return a value and can be checked for exhaustiveness:

const message = switch (state.kind) {
  case "loading":
    "Loading…"
  case "ready":
    state.value.title
  case "failed":
    state.error.message
}

The clauses use ordinary TypeScript syntax. In expression position, the selected case's final expression becomes the switch value, so expression switches do not fall through. Statement-position switches keep TypeScript's existing break and fallthrough behavior.

Blocks and Labeled Breaks

A labeled block can compute a value from an early exit:

const result = parse: {
  if (input.length === 0) break :parse ParseResult.empty()
  if (!isValid(input)) break :parse ParseResult.invalid(input)
  break :parse ParseResult.valid(input)
}

The label makes the destination explicit and allows break to carry a value.

Loop Expressions

Loops can produce a value when a labeled break succeeds:

const firstEven = search: for (const value of values) {
  if (value % 2 === 0) break :search value
} else -1

The else expression runs when the loop completes without breaking to its value label.

Runtime-sized loops remain ordinary runtime loops. Comptime-known loops may be evaluated or unrolled when used from comptime code.

Throw Statements

VibeLang keeps JavaScript statement-form throw; it does not add a throw expression:

function readPort(config: Config): Result<number, InvalidConfig> {
  return if (config.port !== undefined) {
    config.port
  } else {
    throw new InvalidConfig("port")
  }
}

Inside a Result-returning function, the compiler lowers the Error to the Result error variant.

Declarations in Conditions

VibeLang plans to adopt declarations in conditionals from TC39 where appropriate:

if (const user = cache.get(id); user !== null) {
  render(user)
}

The binding stays scoped to the conditional construct.

defer and errdefer

Cleanup belongs next to acquisition:

function write(
  path: string,
  contents: string,
): Result<void, FileError> {
  const fs = FileSystem.context()
  const file = fs.open(path, "write").unwrap()
  defer file.close()
  file.write(contents).unwrap()
}

defer runs when the current scope exits, regardless of the path. errdefer runs only when the scope exits with a Result error:

const database = Database.context()
const transaction = database.begin().unwrap()
errdefer transaction.rollback()
transaction.commit().unwrap()

The exact interaction with defects, async suspension, and disposal protocols will be finalized in the normative specification.