Error Returns and Inference¶
Accepted
Accepted for V1 error-return source semantics and signature inference boundaries.
Error Return Types¶
Error-returning functions use postfix ! on the success type:
fn read(path: Path) []u8!ReadError
This keeps normal data flow visually primary while preserving explicit error flow.
The general form is:
SuccessType!ErrorType
The error type may be omitted for inference:
SuccessType!
For example:
fn read(self: *Self, buf: []u8) usize!
This means the success type is explicit and the compiler infers the error set from the function body.
The explicit top error-set Type value is Error:
fn read(path: Path) []u8!Error
T!Error accepts any error value. Public APIs should prefer named closed error sets when the possible cases are part of the API contract. Use Error when the API deliberately erases or accepts any error.
void!E is valid for functions that return no success value but may fail:
fn save(path: Path, bytes: []const u8) void!WriteError
void! is valid for a function with no success value and an inferred error set.
Catalyst should not use a bare !E return type. Keeping the success type explicit preserves the success-first reading.
Composed error types in the error slot require parentheses:
fn load(path: Path) Module!(ReadError | ParseError)
Anonymous error sets in the error slot also require parentheses:
fn parse(source: Source) Ast!(error { InvalidSyntax })
Error Type Inference¶
An explicit success type with an omitted error type, such as T!, infers only the error set. This is useful when the success contract matters but the exact local error set is derived from implementation.
T! inference collects existing error sets that can leave the function body:
fn load(path: Path) Module! {
var bytes = try read(path)
parse(bytes)
}
The inferred error set is the closed union of contributed error sets. Contributions include try, fallible tail expressions, returned fallible expressions, explicitly returned qualified error values, and errors propagated from inside catch blocks.
Expression-bodied functions use the same final-expression rule. A fallible expression after => contributes to T! inference and must be compatible with an explicit T!E return annotation.
Inference does not create local error sets from bare cases:
fn load(path: Path) Module! {
return .InvalidModule
}
Use a declared error set when the function owns domain errors:
const LoadError = ReadError | error {
InvalidModule,
}
fn load(path: Path) Module!LoadError {
return .InvalidModule
}
A qualified existing error value is valid in an inferred-error function and contributes its static error set:
fn load(path: Path) Module! {
return LoadError.InvalidModule
}
If inference has no contributed errors, it infers the canonical empty error set. This is valid but lintable as errors/unnecessary-fallible-empty-error when the fallible shape is unnecessary.
If any contributed error set is top Error, the inferred error type is Error.
Public APIs with inferred success or error types are lintable as api/public-inferred-signature. Public inferred error sets are lintable even when all contributed sets are named, because the public contract can drift when implementation changes. Public inference that resolves to Error is lintable as api/public-top-error-inference and can suggest spelling T!Error explicitly if top-error erasure is intended.
An ordinary fn whose inferred success type is comptime-only is valid, but lintable as api/implicit-comptime-only-function. Spell comptime fn when the function is intended to be compile-time-only, or add an explicit runtime return type when the value should cross a runtime boundary.
Exception: public type and contract factory functions may infer a Type return when the body is a type expression such as struct, enum, contract, an enum/error-set union, or an intersection. This is the preferred generic type style.
Function type expressions and other bodyless forms have no body, so they require a complete return type and error type. Required contract operation signatures also require explicit error types. Recursive or mutually recursive error inference cycles require an explicit error set in V1. See Function Types and Pointers.
Exported C ABI functions follow normal inference rules, but inferred ABI surface is lintable as api/inferred-c-abi-surface when the resolved signature is otherwise ABI-safe. Any resolved T!E in a V1 C ABI signature is a hard ABI-safety error. See C ABI Interop.
Main Entry Point¶
main may return void, void!E, u8, or u8!E.
fn main() void!MainError {
try run()
}
If main returns a u8 success value, the driver uses that value as the process exit code. If main returns an error, the driver maps it to process failure. Rich messages still require explicit diagnostics or logging.
Diagnostics Are Separate¶
Runtime errors should stay cheap. Rich diagnostics belong in an explicit diagnostic value:
fn read(diag: *Diagnostics, path: Path) []u8!ReadError
Diagnostics are for users, editors, compilers, and tooling; they are not part of normal runtime error payloads.
Error-return types are not V1 C-ABI-safe. The detailed validation rule is owned by C ABI Interop.