VIP-5: try runs an Effect; await is only for Promises
Status: accepted by Will, 2026-10-28.
Decision
async function get(q) { return await queryDb(q) } // await: Promises only, exactly as in JavaScript
effect function load(id) {
const u = try fetchUser(id) // runs the Effect; a failure propagates
const g = fetchUser(id) catch () => guest // runs it; on a failure, gives guest
const h = fetchUser(id) catch effect (e) => match (e) {
when NotFound: guest;
when DbError: throw e; // fails again, typed (VIP-4): the handler is an effect function
}
try sleep("1 second") // an Effect that can't fail is still run with try
await queryDb(id) // expect-error: EK0102; await belongs to async functions
}-
try eruns the Effecteinside aneffectfunction or block and gives its success value. A failure propagates to the enclosing Effect, as Zig'stry xmeansx catch |e| return e. -
Amended 2026-10-30: completed Effects and scheduling.
try euses the acceptedyield* eexecution under Effect's scheduler; it adds no Promise job, implicityieldNowor mandatory suspension. A synchronously completing operand may continue in the same turn, while suspension or the cooperative scheduling budget can run other fibers. Theawaitanalogy governs source placement, precedence and evaluation order. Explicittry yieldNowrequests a cooperative yield without promising a global order for unrelated fibers. See Running Effects. -
Amended 2026-10-30: corresponding execution contexts. Comparisons with
awaitpair anasyncbody with aneffectbody; neither operator becomes permitted in the other kind of function, and transparent expressions inherit their source context. -
e catch yrunse, and on a failure evaluatesy, as Zig'scatchtakes a value or|err|. As in Zig, you writetry xorx catch y, never both. Amended 2026-10-29: the handler's syntax decides whether it runs Effects, as VIP-1 decides for any function:Written Effect TS x catch (e) => f(e)(a plain function; athrowinside isEK0401, VIP-4, amended 2026-10-31)Effect.catch(x, (e) => Effect.succeed(f(e)))x catch effect (e) => …(an effect function; it runs its Effects, andthrowfails again, typed)Effect.catch(x, (e) => Effect.gen(…))Recorded under Will's standing approval of recommendations, after the differential fuzz suite found that the earlier example's
throwwas a defect. Superseded 2026-10-29 15:57: the handler is always a function literal (below). -
Amended 2026-10-29:
catchbinds like??. It has the same precedence and is left-associative:a catch b catch cmeans(a catch b) catch c, andx = a catch bassigns the result. Mixing it with??,||or&&without parentheses is a compile error, as JavaScript requires for??. Recorded under Will's standing approval of recommendations. -
tryis only for Effects. A call that may throw, and a Promise, is first wrapped into an Effect with Effect's own functions (VIP-6:Effect.try,tryPromise), then run withtry. -
An
effectfunction has notry/catchstatement. It handles failures withcatchandmatch. -
Amended 2026-10-29: no
trystatements of any kind,try/finallyincluded. Cleanup uses Effect'sensuringoronExit(VIP-6 globals):try ensuring(write(x), close()). Effect code has exactly onetry, the one that runs an Effect. Recorded under Will's standing approval of recommendations. -
awaitkeeps exactly JavaScript's meaning and belongs only toasyncfunctions:awaitinside aneffectfunction is an error;awaiton an Effect inside anasyncfunction is an error.
-
An Effect that can't fail is still run with
try, and that is fine.
Corrected 2026-10-29: the examples call queryDb, a Promise-returning function imported from a .ts module, because VIP-33 forbids platform calls such as fetch anywhere in a .vibe file.
- Amended 2026-10-29: a non-literal function fallback. In
x catch f, wherefis a non-literal function value, the fallback is the valuef: syntax decides. A VIP-38 lint warning, with quick fixes tocatch (e) => …orcatch effect { … }, flags a fallback whose type is an Effect or a function. Sent to Will on Telegram 2026-10-29 11:28 under his standing rule 'assume I accept'; no objection by 12:00. Superseded 2026-10-29 15:57: a value fallback no longer exists (below). - Amended 2026-10-29:
tryworks likeawait. It is a unary operator withawait's precedence, and whereawaitcan't appear in anasyncfunction (its parameter list, a class field initializer, astaticblock),trycan't appear in aneffectfunction. Will, 2026-10-29: "try works like await." - Amended 2026-10-29: annotated handler parameters. In
catch (e: NotFound) => …, the parameter's type annotation is allowed and checked as for a TypeScript arrow parameter: the error type must be assignable to it (the old EK2112 no longer fires for it). Sent to Will on Telegram 2026-10-29 12:28 under his standing rule 'assume I accept'; no objection by 13:00. - Amended 2026-10-29:
await fiber. In async code,awaiton aFiberhas TypeScript's behavior, plus a VIP-38 lint warning suggestingjoin(EK4609 becomes that lint). Sent to Will on Telegram 2026-10-29 12:28 under his standing rule 'assume I accept'; no objection by 13:00. - Amended 2026-10-29:
await deferred. In async code,awaiton aDeferredhas TypeScript's behavior, plus a VIP-38 lint warning suggestingtry Deferred.await(d)(EK4610 becomes that lint). Sent to Will on Telegram 2026-10-29 12:28 under his standing rule 'assume I accept'; no objection by 13:00. - Amended 2026-10-29: the handler is always a function literal. Its syntax decides its kind:
x catch (e) => v(also function expressions) is plain, and its result is the fallback value;x catch effect (e) => …runs an Effect and may fail again.x catch () => guestreplaces the value formx catch guest, which is removed. A named function needs a wrapper,x catch (e) => f(e), so syntax alone decides. This replaces the handler table above (includingx catch effect { … }) and the non-literal function fallback amendment. Corrected 2026-10-30: this amendment also listedasyncarrows as plain handlers; the amendment below makes them syntax errors (decided detail 85; found by the round-19 Fable review). Decided by Will, 2026-10-29 15:57: "It asked me to confirm a decision I already made." - Amended 2026-10-29: a throwing call in a plain handler. A known throwing call in a plain
catchhandler's eager parameter/default/destructuring initialization (computed binding keys included) or body is a compile error, with the quick fixcatch effect (e) => try Effect.try(() => …). An explicitthrowin a plain handler keeps its defect meaning. Use nonthrowing effect-handler parameters and bridge throwing initialization inside its body; keeping the throwing work in its parameters is still an eager-parameter error. Implicit initialization exceptions retain their defect policy, and merely defining a nested source function adds no initialization calls. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 39). - Amended 2026-10-29:
(try x) catch h.(try x) catch his allowed: it catches the Effect thatxsucceeds with. "Never both" covers only the unparenthesizedtry x catch h. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 61). - Amended 2026-10-29: a plain handler returning an Effect. A plain
catchhandler whose result type is an Effect gets a VIP-38 lint warning, with the quick fixcatch effect (e) => try …:fetchUser(id) catch (e) => fetchUser("guest")gives the unrun Effect. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 62). - Amended 2026-10-29: no generator or
asynchandlers.asyncarrows,asyncfunction expressions, generator expressions and async generator expressions are syntax errors ascatchhandlers, with the quick fix "removeasync" or "remove*". Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 85). - Amended 2026-10-29: one test for an unrun Effect or Promise.
EK0103,EK0508,EK0404andEK0507ask whether a value may be an unrun Effect (or Promise): a type answers yes when it is notnever,anyorunknownand at least one union member is an Effect (orPromiseLike).try,catchandtry.allkeep assignability. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 89). - Amended 2026-10-29:
forkandjoinheads beforecatch.EK0505covers afork,fork.in(s),fork.scoped,fork.detachedorjoinreachable from acatchhead through unary and binary operator nodes only, by the same rule astry:join f catch hisEK0505(writeFiber.join(f) catch h), andfork e catch hisEK0505(writefork Effect.catch(e, ($e) => Effect.succeed(h($e))), orfork Effect.catch(e, h)for aneffecthandler, soestays on the caller and the handler runs on the forked fiber;fork (e catch h)runsehere and forks its result,EK1703unless that result is an Effect). Corrected 2026-10-30: the quick fix wasfork (e catch h), which contradicted decided detail 99's own Effect TypeScript,Effect.forkChild(Effect.catch(e, …))(found by the round-19 Fable review).(join f) catch hand(fork e) catch hare notEK0505;(fork e) catch his stillEK0506, as aFiberis not an Effect in Effect 4.1.1. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 99). Corrected again 2026-10-30: the fix wrappedein aneffectblock, which movedeonto the forked fiber, so a failure oftry id()inestopped being the caller's.fork eevaluateseon the caller, asconst p = work(await id())does. - Amended 2026-10-30: the value and effect-block rows are removed. The table rows
x catch guestandx catch effect { … }, removed from the table above, are superseded by the function-literal rule (decided detail 31a): writex catch () => guestandx catch effect () => { … };x catch effect { return 7 }is a syntax error. Recorded by the agent from Will's decision 31a (decided detail 31b).
Corrected 2026-11-02: the Decision example's handler is catch () => guest; the value form catch guest it showed was removed by the 2026-10-29 amendment above (found by the round-7 review).
Corrected 2026-10-29: the example ends every match arm with ;, the last one included, as the pinned TC39 grammar requires; a newline is not a separator (found by the round-3 review).
-
Amended 2026-10-30: unchecked operands retain native delegation. After the static Effect-assignability check,
try eis stillyield* e, with no dynamic Effect guard, Promise conversion or non-Promise identity. An unchecked noniterable produces its nativeTypeErroras a defect. An unchecked iterable can instead complete normally, delegate Effects or return a value; no blanket rejection of all runtime non-Effects is added. Ordinary typed arrays and generators still fail the static operand check. See Running Effects and the runtime witness. -
Amended 2026-10-30: diagnose the missing handler function first. A parameter list followed directly by a block after
catch, as intry x catch (e) { B }, is the existingEK0509malformed-handler error. Do not also reportEK0505for that same incomplete handler. Once a function literal is present, the ordinary head check applies; independent context and separate syntax errors remain. Repair explicitly tox catch effect (e) => { B }or a plain fallbackx catch (e) => { B }under their distinct existing throw/result rules. A mechanical repair must preserve valid parameters, bindings and source jump targets; it does not create block-handler syntax or auto-run a plain handler's returned Effect. See the handler rule. -
Clarified 2026-10-30: warning repairs keep the source callable's context. The
await fiberandawait deferredamendments above preserve nativeawaitbehavior. In an ordinaryasyncfunction, the explicit-bridge rule of VIP-1 and this VIP givesawait runPromise(Fiber.join(fiber))andawait runPromise(Deferred.await(deferred))when the author wants the completion value. Their Effect-body forms,join fiberandtry Deferred.await(deferred), are outside-effect errors in thatasyncbody. This corrects the repair wording, not warning severity or execution: reporting a warning does not start either value, and a repair does not silently change the enclosing function's kind. -
Amended 2026-10-30: return values stay unflattened. An
effectfunction returns its body value asEffect.gensuccess data. A returned inner Effect stays unrun:return ekeeps it, whilereturn try eruns it and returns its success. A successfultryand a plaincatchhandler result likewise keep their immediate value as data, without recursive execution or thenable assimilation. Anasyncreturn instead assimilates its returned Promise or thenable. This records the existing VIP-1 counterpart rather than adding implicit execution. -
Amended 2026-10-30: erased TypeScript wrappers in a
catchhead. TheEK0505unary/binary head search treatsas, admitted angle-bracket assertions,satisfies, non-null!and type-only instantiation expressions without a call as transparent. Explicit source parentheses remain a boundary; emitter-added grouping does not count. The wrappers keep native type checks and do not change the existing call, literal or source-function boundaries. See the checked examples. -
Amended 2026-10-31: a
throwin a plain handler isEK0401(VIP-4). A call known to throw in its parameters or body is too. The quick fix iscatch effect, which makes the throw a typed failure. The old lint warningEK0511retires. -
Amended 2026-10-31: the remaining
catchgrammar code.catchmixed with??,||or&&without parentheses, and a member access or call after any closed handler (a parenthesized handler, a function-expression handler or a block-bodied arrow handler), areEK0512; parenthesize the whole catch expression to attach that access or call to its result.EK0504toEK0510keep their specific triggers;EK0511is retired (above). Decided by the agent under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 247). -
Clarified 2026-11-03: every closed handler. The member access or call in the
EK0512trigger is the one after any closed handler: a parenthesized handler, a function expression or a block-bodied arrow, as the Running Effects prose and Appendix A'sClosedCatchExpressionsay since the 2026-11-03 block-bodied arrow correction.a catch () => { }.call(o)isEK0512, not an unassigned syntax error.
Prior art
- Zig's
tryandcatch. - Rust's
?. - Swift's
try, which marks each call that can fail. - Effect's
yield*.
Will's words
"using try and catch like zig seem like they really make a lot of sense to me. Then await can stay a promise thing … So then rather than overloading anything. We don't overload await it just means promise await. We instead [have] try be how you unwrap an effect. And then we have a concise way to wrap an sync or a promise in an effect. Sync that does not throw you can just callIt() normally otherwise it yells at you for not try if it has error type or error is unknown. promise you must wrap in an effect and then you can try it too."
"I don't think effect functions should allow try catch statements nor do they need try catch statements nor would it be idiomatic to use them if we did support them. I think the cost for can't fail is fine."
"Ok I accept that VIP"
"for 2 yea it works pretty much like zig" (on
catchtaking a value or a function of the error)
Footnote: old VibeLang
Old VibeLang is a reference only; nothing in it is part of the language unless a VIP adds it. For implementers, this VIP replaces:
- VIP-1's "await unwraps both". VIP-1 has been updated to match this VIP.
- D540's
await, which ran Effects. - The
try/catchstatement in Effect code.
Later VIPs
-
for awaitover a Stream, andawait.all/await.race, need new spellings. They are decided in their own VIPs. -
Amended 2026-11-01: no guessed operand conversion. A value that fails the structural Effect operand test keeps
EK0502; migration inserts no conversion and unwraps nothing. AnasEffectmember does not grant permission to insert a call.Option.some(1)andResult.succeed(2)fail the test in the pinned Effect version. Removing a proven old wrapper's run is the separate VIP-34 migration rule.
Amendment: inferred Effect arguments
Amended 2026-11-03: VIP-1's dated inferred-argument amendment defines the EK4802 trigger, argument positions, opt-outs, boundaries and repairs. try still runs only an Effect; an editor action checks the enclosing written failure and requirement channels, and batch --fix refuses an edit that adds an error. Explicit type arguments keep program data by the author's choice, never by batch --fix.