CEP-0065: Error-Only Defer¶
Draft
Draft proposal for an errdefer cleanup source form. V1 accepts only unconditional defer; error-only cleanup remains deferred.
Summary¶
Catalyst should consider an error-only cleanup statement modeled after Zig's errdefer: a scoped cleanup registered near acquisition, but executed only when the owning scope exits by propagating or returning an error.
Possible future source form:
var session = try Session.create(alloc)
errdefer session.dispose()
try session.attach_logger()
return move session
In this shape, session.dispose() runs if Session.create succeeds but a later try or return SomeError.Case exits the function with an error. It does not run on the final successful return move session.
Motivation¶
Unconditional defer is ideal for temporary resources that must be cleaned on every exit edge. It is awkward for resources that should be handed to the caller on success but cleaned up on failure.
Without error-only cleanup, code must either duplicate cleanup in each error path or use a broader defer plus an explicit cancellation/state flag. Both patterns separate acquisition from recovery behavior and make it easier to miss one exit edge.
errdefer keeps the cleanup next to the acquisition while preserving the successful ownership transfer.
Proposed Direction¶
errdefer would be statement syntax, not an expression. Like defer, it would register a cleanup action against the current lexical block scope and evaluate the cleanup at scope exit rather than at registration time.
Unlike defer, the cleanup would run only for an error exit edge:
trypropagates an error out of the enclosing function;return SomeError.Caseorreturn errreturns an error value from the enclosing function;- a block form that is later accepted as producing an unhandled
T!Eresult exits through the error arm, if the proposal decides to support block-local error exits.
The cleanup would not run for:
- fallthrough;
- successful
return; breakorcontinue;- an error handled by
catchinside the same scope.
errdefer and defer should share one cleanup stack per lexical scope. On scope exit, registered cleanups are considered in reverse source order; unconditional defer entries always run, while errdefer entries run only when the exit reason is an error.
Error Binding¶
The proposal should consider an optional Catalyst-style error binding:
errdefer as err {
diag.err(err)
session.dispose()
}
The binding would be immutable and scoped to the cleanup block. Its static type should be the outgoing error type visible at that exit edge, following the same compatibility and inference model as try propagation.
If no binding is present, errdefer statement is sufficient for ordinary cleanup:
errdefer session.dispose()
Cleanup statements are checked in a void context. If cleanup itself can fail, the accepted design should require explicit handling or define whether a cleanup error can replace, wrap, or be suppressed in favor of the original outgoing error.
Example¶
Possible future accepted form:
fn open_pair(left_path: Path, right_path: Path) FilePair! {
var left = try File.open(left_path)
errdefer left.close()
var right = try File.open(right_path)
errdefer right.close()
try validate_pair(&left, &right)
return FilePair {
left: move left,
right: move right,
}
}
If the second File.open or validate_pair fails, already-acquired resources are cleaned in reverse registration order. On success, ownership moves into the returned FilePair, and the error-only cleanups do not run.
An error-bound cleanup could record the reason before releasing partially initialized state:
fn build_index(path: Path, alloc: *std.mem.Allocator) Index! {
var index = try Index.create(alloc)
errdefer as err {
diag.err(err)
index.dispose()
}
try index.load(path)
return move index
}
These examples are illustrative only. V1 rejects errdefer.
Open Questions¶
- whether
errdefershould be reserved as a keyword before acceptance or remain unavailable until the feature is accepted; - whether the trigger is strictly "the enclosing function returns an error" or a more general "this lexical scope exits with an unhandled error result";
- whether
errdefer as errshould bind the original outgoing error before any cleanup runs, after narrowing, or separately for each exit edge; - whether cleanup code may use
try, and if so how a cleanup failure composes with the original error; - how resource lints should model success-only ownership transfer through
return move valuewhen an activeerrdefertargets the moved binding; - whether
errdefershould interact with future cancellation or explicit defer-disarm syntax.
V1 Compatibility¶
V1 accepts unconditional defer only. errdefer is not part of the V1 reserved word set, grammar, cleanup semantics, or resource-lint model.
Code that needs V1-compatible error cleanup should use explicit catch blocks or ordinary defer with explicit state.