Expected Errors
Expected errors describe situations callers can handle: invalid input, a denied operation, or an unavailable dependency. VibeLang infers them in the E channel and narrows them inside catch.
Constructing typed failures
// run: Console.log(normalizeCode(" pen ")); Console.log(Effect.flip(() => normalizeCode(" "))._tag)
error InvalidCode {
input: string
}
error ItemMissing {
code: string
}
function normalizeCode(input: string) {
const code = input.trim()
if (code.length === 0) throw new InvalidCode({ input })
return code.toUpperCase()
}
// string!InvalidCode!PEN
InvalidCodeerror InvalidCode { input: string } declares an expected failure. The declaration is a class: its constructor takes the fields, every field is readonly, and each instance has a readonly _tag equal to the class name. In an Effect function, throw adds the class to E. error Timeout declares an error without fields. The declaration is short for class InvalidCode extends TaggedError("InvalidCode")<{ readonly input: string }> {}, which also compiles; the global TaggedError is Effect's Data.TaggedError. Effect.fail(error) is also available, and Effect.failSync(() => error) defers construction until execution, though a function body already gives you that deferral.
In Effect functions, trusted built-ins can add expected failures too: JSON.parse adds SyntaxError. A plain TypeScript function needs the try hatch or a wrapper created with trusted(implementation, [ErrorClass]) in a .ts module. Import trusted from vibelang-runtime: instances of the listed classes join E, and unlisted exceptions are defects. The older /** @vibelang trusted */ and @throws declaration tags still work. Calls that rely on tagged expected failures produce migration warning EK4406. Effect's own packages are trusted already, so calls such as Option.isNone need no boundary.
A function with a plain contract keeps JavaScript exceptions (D263): an explicit non-Effect return annotation, or a plain contract inferred by the checker, makes throw a synchronous JavaScript throw inside and outside try. For example, (value: string): string => { if (!value) throw new Error("empty"); return value } returns a string or throws immediately, and its declaration stays (value: string) => string. Plain-function parameters and TypeScript imports are ordinary calls there, with no trust boundary required. Exceptions become defects only where they escape into an effectful caller. Calling an Effect inside a plain-contract function is still EK2003.
Failing in an expression
// run: Console.log(port(new Map([["PORT", "3000"]]))); Console.log(Effect.flip(() => port(new Map()))._tag)
error MissingPort
function port(env: Map<string, string>) {
return env.get("PORT") ?? throw new MissingPort()
}
// string!MissingPort!3000
MissingPortthrow also works in expression position, as the TC39 throw expressions proposal writes it, and fails the same way the statement does. The right side of ??, ||, &&, and ?: runs only when it is chosen.
Tracking several error cases
// run: Console.log(findItem("pen").label); Console.log(Effect.flip(() => findItem("book"))._tag); Console.log(Effect.flip(() => findItem(""))._tag)
function findItem(input: string) {
const code = normalizeCode(input)
if (code !== "PEN") throw new ItemMissing({ code })
return { code, label: "Fountain pen" }
}
// { code: string; label: string }!InvalidCode | ItemMissing!Fountain pen
ItemMissing
InvalidCodeA function collects the failures it throws and those its calls can produce. Sequential statements stop at the first failure, so no lookup happens after normalization fails. An explicit full contract checks this inferred union rather than replacing it.
Turning failures into values
// run: Console.log(inspectItem("pen")); Console.log(inspectItem("")); Console.log(inspectItem("book")); Console.log(optionalItem("pen")); Console.log(optionalItem("book"))
function inspectItem(input: string) {
const result = Effect.result(findItem(input))
return result.match({
onFailure: (error) => `unavailable: ${error._tag}`,
onSuccess: (item) => item.label
})
}
function optionalItem(input: string) {
const item = Effect.option(findItem(input))
return item.isNone() ? "No item" : item.value.label
}Fountain pen
unavailable: InvalidCode
unavailable: ItemMissing
Fountain pen
No itemEffect.result returns Result<A, E> in the success channel and removes the typed failure channel. Effect.option returns Some on success and discards a typed error as None. Use the latter only when every domain failure means absence. Causes without any expected failure still propagate; these Effect combinators can consume a mixed cause when it also contains an expected failure. Use ordinary catch when defects and interruption must always propagate intact.
Recovering all failures
// run: Console.log(itemLabel("pen")); Console.log(itemLabel("")); Console.log(itemLabel("book"))
function itemLabel(input: string) {
try {
return findItem(input).label
} catch (error) {
return `Unavailable (${error._tag})`
}
}
// string!!Fountain pen
Unavailable (InvalidCode)
Unavailable (ItemMissing)The catch receives InvalidCode | ItemMissing and handles both here. It handles only causes with no defect and no interruption. If several expected failures occur together, it receives the first and handles their whole cause.
// run: Console.log(itemLabelInline("pen")); Console.log(itemLabelInline("")); Console.log(itemLabelInline("book"))
function itemLabelInline(input: string) {
return findItem(input).label catch (error) => `Unavailable (${error._tag})`
}
// string!!Fountain pen
Unavailable (InvalidCode)
Unavailable (ItemMissing)catch also works after an expression. x catch (error) => handler runs x and passes an expected failure to the handler, as Effect.catch does, with the same guard as the statement: defects and interruption pass through. x catch fallback evaluates the fallback instead, as Effect.orElseSucceed does; see Fallback.
Effect.catch(() => findItem(input), handler) also recovers expected failures, but keeps Effect's own policy: it can recover a mixed cause containing an expected failure and drop the other reasons. Effect.catchEager uses that same policy and can apply recovery immediately when given an already computed Exit. Neither combinator has source catch's mixed-cause guard.
Recovering selected failures
// run: Console.log(labelOrMissing("book")); Console.log(Effect.flip(() => labelOrMissing(""))._tag); Console.log(labelByTag("book").label); Console.log(Effect.flip(() => labelByTag(""))._tag)
function labelOrMissing(input: string) {
try {
return findItem(input).label
} catch (error) {
return match (error) {
when ItemMissing: "Out of catalog";
default: throw error;
}
}
}
// string!InvalidCode!
function labelByTag(input: string) {
return Effect.catchTag(
findItem(input),
"ItemMissing",
(error) => ({ code: error.code, label: "Out of catalog" })
)
}Out of catalog
InvalidCode
Out of catalog
InvalidCodeCatch the failure, then match on it: an arm such as when ItemMissing: handles that error class, and default: throw error passes the others on with their types, as string!InvalidCode! shows. The expression form takes the same match: findItem(input).label catch (error) => match (error) { when ItemMissing: "Out of catalog"; default: throw error; }. A match without default must handle every failure the try block can raise, or it is a compile error (EK4701), so a complete match needs no rethrow. error is ItemMissing tests one class in an if. Effect.catchTag removes the selected tagged case; it also accepts a nonempty array of tags sharing one handler. Effect.catchTags supplies a table of separate handlers.
// run: Console.log(withTagTable("").label); Console.log(withFilter("book").label); Console.log(withPredicate("book").label); Console.log(Effect.flip(() => withFilter(""))._tag); Console.log(Effect.flip(() => withPredicate(""))._tag)
function withTagTable(input: string) {
return Effect.catchTags(findItem(input), {
InvalidCode: () => ({ code: "", label: "Enter a code" }),
ItemMissing: (error) => ({ code: error.code, label: "Out of catalog" })
})
}
function withFilter(input: string) {
return Effect.catchFilter(
() => findItem(input),
Filter.tagged("ItemMissing"),
(error) => ({ code: error.code, label: "Out of catalog" })
)
}
function withPredicate(input: string) {
return Effect.catchIf(
() => findItem(input),
(error) => error is ItemMissing,
() => ({ code: "", label: "Out of catalog" })
)
}Enter a code
Out of catalog
Out of catalog
InvalidCode
InvalidCodecatchIf selects errors using a predicate or type guard. catchFilter accepts reusable selection logic from Filter; a narrowing filter removes the handled subtype. A boolean condition that handles only some values cannot promise that the whole subtype is gone.
Filtering nested reasons
// run: Console.log(explainBusy()); Console.log(explainReasons()); Console.log(Effect.flip(() => exposeReasons())._tag)
error Busy { seconds: number }
error Rejected
error CatalogError {
reason: Busy | Rejected
}
function remoteLabel(): string!CatalogError! {
throw new CatalogError({ reason: new Busy({ seconds: 4 }) })
}
function explainBusy() {
return Effect.catchReason(
() => remoteLabel(), "CatalogError", "Busy",
(reason) => `Try again in ${reason.seconds} seconds`
)
}
function explainReasons() {
return Effect.catchReasons(() => remoteLabel(), "CatalogError", {
Busy: (reason) => `Wait ${reason.seconds} seconds`,
Rejected: () => "Request rejected"
})
}
function exposeReasons() {
return Effect.unwrapReason(() => remoteLabel(), "CatalogError")
}
// string!Busy | Rejected!Try again in 4 seconds
Wait 4 seconds
BusyThese operators inspect the tagged error's readonly reason field. catchReason keeps the parent wrapper for unhandled reasons, while catchReasons selects several reasons at once. unwrapReason replaces the wrapper with its nested reason in E without recovering.
Coming from Effect
Functions replace Effect.gen; calls run the callee. Pass a program such as () => findItem(input) when a recovery operator needs work it can control.
The matching Effect page: effect.website/docs/v4/error-management/expected-errors