Skip to content
LogoLogo

Results and Errors

Return expected failures as values

VibeLang uses ordinary TypeScript-shaped code and makes expected failure explicit in the return type. A synchronous fallible function returns Result<A, E>; an asynchronous one returns Promise<Result<A, E>>.

Declare an Error

Expected failures are normal Error subclasses:

class NotFound extends Error {
  constructor(readonly id: string) {
    super(`Not found: ${id}`)
  }
}
 
class RateLimited extends Error {
  constructor(readonly retryAfter: Duration) {
    super("Rate limited")
  }
}

No tagged-error factory, special declaration, or _tag field is required. The compiler derives stable nominal identity and transport metadata while retaining instanceof, cause, stack traces, and ordinary JavaScript error behavior.

Write a Fallible Function

Annotate the ordinary return type with Result:

function readSettings(
  path: string,
): Result<Settings, FileNotFound | PermissionDenied | InvalidSettings> {
  const fs = FileSystem.context()
  const source = fs.readText(path).unwrap()
  return Settings.parse(source).unwrap()
}

Inside a Result-returning function, VibeLang lowers plain return value to the success variant and throw error to the error variant. Returning an existing compatible Result does not nest it. Consequently source code does not need Result.ok or Result.err, and those constructors are not part of the authoring API.

When a body has no annotation, the compiler infers A from returned values and E from reachable Error throws, returned Results, and .unwrap() propagation points.

Propagate With .unwrap()

Result.unwrap() is compiler-aware control flow:

function loadApplication(): Result<Application, ReadSettingsError> {
  const settings = readSettings("app.json").unwrap()
  return Application.make(settings)
}

On success it produces the value. On error it returns that error from the enclosing Result-returning function. In plain JavaScript without VibeLang lowering, it throws the original error so a missed compiler transform fails visibly.

Results are must-use values. Discarding a Result is a diagnostic unless the code explicitly acknowledges it.

Inspect and Transform a Result

Use normal methods for value-oriented handling:

const label = readSettings("app.json").match({
  ok: settings => settings.name,
  error: error => `unavailable: ${error.message}`,
})
 
const port = readSettings("app.json")
  .map(settings => settings.port)
  .unwrapOr(3000)

The initial quality-of-life surface is:

  • isOk(), isError(), match(...)
  • map(...), mapError(...), andThen(...), recover(...)
  • tap(...), tapError(...)
  • unwrap(), unwrapOr(...), expect(...)
  • Result.all(...), Result.try(...), and Result.tryPromise(...)

Result.try and Result.tryPromise exist for foreign JavaScript boundaries that may throw or reject. They are not success/error constructors.

Match Specific Errors

Every Error has compiler-supported helpers:

const message = readSettings("app.json").match({
  ok: settings => `loaded ${settings.name}`,
  error: error => error.match({
    FileNotFound: error => `missing ${error.path}`,
    PermissionDenied: error => `cannot read ${error.path}`,
    InvalidSettings: error => error.message,
  }),
})

For a known error union, error.match is exhaustive and narrows each handler argument. error.matchPartial(handlers, fallback), error.is(Constructor), error.matches(...), and error.rootCause() support partial matching and inspection. Dispatch uses compiler-provided nominal identity, not a user-maintained string tag.

Async Functions

await unwraps only the Promise. The Result remains visible:

async function loadUser(
  id: string,
): Promise<Result<User, Timeout | InvalidResponse>> {
  const http = HttpClient.context()
  const response = (await http.get(`/users/${id}`)).unwrap()
  return (await response.json(User)).unwrap()
}

Authored VibeLang bans Promise instance .then(), .catch(), and .finally(). Use await, then handle the resulting Result. Imported JavaScript and TypeScript libraries may use ordinary Promise internals behind their boundary.

Defects and Foreign Exceptions

Expected domain errors belong in E. A failed compiler/runtime invariant uses Reflect.panic(...) and is not converted to a Result. An undeclared exception or Promise rejection crossing a foreign boundary becomes UnhandledException unless the boundary maps it to a specific Error subclass with Result.try or Result.tryPromise.

JavaScript statement-form try/catch remains available for low-level interop. It is not the typed recovery API.

Cleanup

errdefer runs when a scope exits with the error variant:

function replaceFile(
  path: string,
  contents: string,
): Result<void, FileError> {
  const fs = FileSystem.context()
  const temporary = fs.createTemp(path).unwrap()
  defer temporary.close()
  errdefer fs.remove(temporary.path)
 
  temporary.write(contents).unwrap()
  fs.replace(temporary.path, path).unwrap()
}

defer runs on every exit path; errdefer runs only for Result-error exits.