Skip to content

Protos Errors v0.1

Canonical source: spec/semantics/ERRORS.md at locked revision e99b712ecf658a527b1fe82c8beba2c1286d8dc9. The source document’s own status and authority declarations apply; this website rendering does not upgrade, downgrade, or redefine them.

Language version: 0.1 Status: Draft Last updated: 2026-09-18

This document is the primary normative owner of Core Error objects, signaling, handling, propagation, identity, standard error-prototype taxonomy, and error-control semantics.

The material below is migrated without intended semantic change from ../PROTOS_LANGUAGE_SPEC.md. Legacy section titles and numbering are retained so existing references remain understandable.

Errors are objects.

Expected domain failures may be represented using ordinary values.

Exceptional conditions use signaling:

error.signal()

The standard Error prototype provides the ordinary zero-argument message signal().

An ordinary error object may therefore be signaled with:

error.signal()

The standard behavior is inherited through ordinary delegation. Its receiver must be Error itself or an object whose delegation chain contains Error. Calling the standard signaling behavior with any other receiver is a protocol error; copying, composing, extracting, or otherwise reusing the implementation of Error.signal does not make a non-error object signalable.

The receiver object itself is the object presented to handler matching and to the selected handler. Signaling does not implicitly clone, wrap, replace, or convert it, and does not add Protos-visible slots or mutate its ordinary language-visible state. Implementations may retain implementation-private diagnostic information such as stack metadata only when that information is not observable as additional Core object structure or taxonomy.

Error.signal() never returns normally to the activation that invoked it. If a matching handler is found, Core’s unwinding semantics transfer control to that handler and abandon the signaling continuation. If no matching handler is found, the error reaches the applicable outermost execution boundary according to the existing unhandled-error rule.

Standard failure occurrence objects and identity

Section titled “Standard failure occurrence objects and identity”

Core distinguishes signaling an Error object that already exists from a standard language/runtime/domain failure rule that must manufacture an Error outcome.

A standard failure occurrence is one execution of a normative failure rule for which the specification does not already identify a particular Error object to propagate. Every standard failure occurrence produces a fresh ordinary Error object with fresh identity:

  • if the rule says only that it “signals an Error”, “signals an error”, or otherwise promises no narrower standard category, the fresh object’s immediate delegation parent is the standard Error prototype;
  • if the rule says that it signals/fails with a named standard error prototype X, the fresh object’s immediate delegation parent is X;
  • the generated object is never Error, X, or another standard category prototype itself, and it is never an Error instance reused from a distinct standard failure occurrence.

Core v0.1 therefore has no singleton standard failure instances. Standard Error prototypes are shared category/protocol objects, not cached instances for runtime failures. Two independently occurring lookup failures, argument failures, cancellation observations, I/O failures, or other standard failures are distinct by === even when they have the same standard prototype ancestry.

Freshness is an observable semantic requirement, not a physical-allocation requirement. An implementation may scalar-replace, virtualize, elide, or otherwise optimize a generated Error when doing so cannot affect any Core observation. It must nevertheless preserve distinct identity whenever identity can become observable through ===, identity hashing, handler installation, storage, Future failure, reflection defined elsewhere, or ordinary object reachability. Pooling or caching that makes two distinct standard failure occurrences observe the same object is non-conforming.

Conversely, when semantics already identifies an Error object e, signaling or re-signaling that failure uses exactly e. In particular:

  • e.signal() signals e itself;
  • a handler receives that exact e;
  • a same-domain failed Future records and later re-signals that exact e under the Future rules;
  • re-signaling an Error never manufactures a new instance merely because a new handler search begins.

Error and every standard Error prototype are themselves ordinary Error objects and are legal explicit receivers of signal(). Thus source code may explicitly execute Error.signal() or Cancelled.signal(), in which case the receiver prototype itself is the exact signaled object. That explicit user-directed act does not authorize a standard failure rule to reuse the prototype as its failure instance.

Unless a domain contract explicitly defines visible Error payload slots, a standard-generated failure instance is not required to have any own Protos-visible slot. Diagnostic messages, native error codes, stack data, paths, handles, authority-bearing backend objects, and similar implementation data must not become visible merely because an implementation finds them useful for logging. A separately standardized diagnostic/reflection facility may expose only what its own contract permits.

Error identity and hashing follow the ordinary object identity rules. Once a particular Error object exists, its identity is stable for as long as the object is observably reachable. identityHashOf(error) therefore has the same stability and collision rules as for other objects; fresh Error identity does not imply a unique numeric hash value.

The standard prelude provides fail as an ordinary callable that accepts exactly zero supplied arguments. A reached valid fail() invocation is one standard failure occurrence under the rules above: it creates one fresh ordinary Error object whose immediate delegation parent is the canonical standard Error prototype for the current Core environment, then enters the existing semantic signaling operation with that exact fresh object. The invocation never returns normally.

The standard fail callable does not obtain Error through caller lexical lookup. A caller-local binding named Error therefore cannot change the category created by fail(), and extracting, storing, or passing the fail callable does not change its canonical Error provenance.

Each reached valid invocation is distinct. Implementations must not pool or reuse a singleton Error where ordinary Error identity could observe two fail() occurrences as identical.

Supplying any argument is not a valid fail() invocation. Such a call follows the ordinary standard-callable arity-failure path, and the generic failure occurrence described above must not be performed first.

fail() adds no message, cause, payload, existing-Error, Error-prototype, rethrow/current-error, custom-subtype, or guest-visible stack/source facility. It adds no Error taxonomy category and does not change Error.signal(), error.signal(), handler selection, exact re-signaling identity, unwinding, ensure, Future failure identity, Actor/P boundary behavior, or the non-resumable Error model.

Freshness is defined per failure occurrence in the value/isolation domain where that occurrence is created. It does not create a cross-boundary identity channel.

When an Error is transferred as ordinary data or as a P failure through a boundary whose value-transfer contract reconstructs object graphs, the receiving domain gets the boundary-defined reconstructed Error object. The source-domain Error and reconstructed receiving-domain Error are not identical. Aliasing and cycles inside one transferred graph follow that boundary’s ordinary graph-transfer rules; no special remote identity, proxy, or Error singleton is introduced.

Actor fatal failure is different: the unhandled Error that terminates an Actor incarnation remains in that Actor domain. It is not implicitly transferred, wrapped, copied, or re-signaled to a caller. A caller observes only the communication/lifecycle outcome promised by the Actor contract, such as a fresh caller-domain RequestOutcomeUncertain occurrence when applicable.

When a domain explicitly records one Error as one stable failure outcome, later same-domain observations of that recorded outcome preserve the exact recorded Error rather than creating fresh failures. This applies to failed(error) Futures and to any I/O lifecycle contract that explicitly records one terminal failure cause. A later operation that merely encounters the same category but is not observing the same recorded outcome is a new failure occurrence and gets a fresh Error object.

Core v0.1 has one standard language-level failure family rooted at Error. It does not define a separate standard Exception hierarchy or a second continuable-exception category.

Error.signal() is non-resumable. Once signaling begins, the continuation at the signaling point is abandoned. A matching handler may determine the result of the enclosing handle(...) operation under the existing unwinding rules, but it cannot return, resume, retry, or supply a value back into the abandoned Error.signal() invocation.

Core v0.1 therefore defines no standard resume, retry, restart, useValue, continuable-signal, or arbitrary continuation-reentry operation for error handling. A user/library object may of course define ordinary messages with such names, but those messages have no privileged relationship to Core error signaling.

This boundary is deliberate rather than a claim that resumable recovery is intrinsically undesirable. A future standard may define explicit recovery continuations or restart-style mechanisms, but doing so requires a separate semantic contract for recovery availability, dynamic extent, effects already performed, suspension, Futures, Actor/P boundaries, and cancellation. Such a future mechanism must not retroactively make Core v0.1 Error.signal() resumable.

The standard method takes no arguments. Constructing or enriching an error is separate ordinary object/protocol behavior and occurs before signaling. Core does not define string-to-error coercion, prototype-to-instance coercion, or any other implicit condition designator.

The runtime’s own language-defined failures use the same semantic signaling operation directly; they are not specified as an overridable message send to signal(). Overriding a user object’s signal slot therefore affects ordinary message dispatch to that object but cannot redefine how slot lookup failures, invalid assignments, cancellation observation, or other normative runtime failures transfer control.

This keeps error signaling inside the ordinary object/delegation model while preserving one closed semantic category of signalable Core errors rooted at Error.

Handlers are dynamically installed in the execution environment of closures.

No fundamental try, catch, throw, or finally keywords are required.

Core v0.1 exposes handler installation through an ordinary message provided by the standard Error prototype:

matchPrototype.handle(body, handler)

matchPrototype is the receiver. Error itself and ordinary error prototypes below Error therefore use the same protocol through normal delegation. The receiver must be Error or have Error in its delegation chain.

body and handler must be semantic Closure values. This protocol is deliberately Closure-only: an ordinary object that is merely ordinarily invokable through a call slot does not satisfy either parameter.

Ordinary call evaluation happens before installation. The receiver expression, the body argument expression, and the handler argument expression are evaluated left-to-right. After all three expressions complete normally, the resulting body value is validated as a Closure, then the resulting handler value is validated as a Closure.

Only after both validations succeed does the call establish exactly one dynamically scoped unwinding handler whose match prototype is matchPrototype, then invoke body with no arguments.

If either validation fails, the call signals the ordinary invalid-argument Error at that validation point. No handler frame has been installed and body is not invoked. Validation of handler is therefore never deferred until or unless body signals an Error.

Errors signaled while evaluating the receiver or argument expressions, or while performing these pre-installation validations, cannot be handled by the handler being installed.

If body completes normally, its result is the result of handle, and the handler is removed without invoking handler.

For handler matching, an Error object’s matching chain consists of the signaled object itself followed by its ordinary delegation parents. A handler matches when its matchPrototype is any object in that chain. Category matching is therefore ordinary delegation matching; exact identity is the degenerate case in which matchPrototype === signaledError. Core defines no second hidden class/tag/identity matching channel.

If an error is signaled while the protected dynamic extent is active and a handler matches by that rule, the dynamically innermost matching handler is selected before any older matching handler. Core handlers are unwinding: execution of the signaling continuation and the remaining protected computation is abandoned. The selected handler frame is removed before handler is invoked, and handler receives the exact signaled Error object as its single argument. If handler then completes normally, its result is the result of the handle call.

Because the selected handler is inactive while its handler Closure executes, an error signaled by that Closure is searched only against still-active outer handlers. The same handler cannot recursively catch its own failure merely because the new error also matches matchPrototype.

Selected handler deactivation precedes unwind cleanup

Section titled “Selected handler deactivation precedes unwind cleanup”

Selecting a matching handler consumes that dynamic handler frame as a one-shot unwind destination before control begins unwinding protected scopes toward the handler boundary. Consumption makes the frame inactive for new matching, but the selected destination is still invoked if the original Error reaches it after all crossed cleanup completes normally.

Therefore any ensure cleanup executed while unwinding toward the selected handler runs with that selected handler already inactive. If an Error transfer initiated by such cleanup escapes the cleanup Closure, that Error follows ordinary handler search among still-active outer handlers and any handlers explicitly installed by cleanup. It cannot select the already-consumed handler frame.

This ordering applies even when the escaping cleanup Error would also match the selected handler’s matchPrototype. The selected destination is abandoned when the escaping cleanup transfer supersedes the original transfer; it is not reused as a handler for the replacement Error.

An Error signaled and completely handled inside cleanup does not escape cleanup. If that cleanup then completes normally, the original Error remains pending and the already-selected destination is invoked with that original Error under the existing rules.

If all crossed cleanup completes normally, the originally selected handler is invoked with the original Error under the existing rules. If an Error transfer from cleanup instead escapes and supersedes the original transfer, the originally selected handler is not invoked for either Error unless some separate still-active installation selects the replacement Error independently.

Nested handle calls define ordering structurally: the dynamically innermost matching handler is selected first. Core v0.1 therefore needs no separate same-scope handler-list ordering rule; multiple handlers are expressed by ordinary nesting.

A nonmatching error passes through the installed frame and continues outward through the normal dynamic handler search. Non-local return, cleanup through ensure, and other existing unwind behavior remain ordinary control transfers: leaving the protected dynamic extent removes the installed handler.

Dynamic handler state is task-local execution state, not Actor-global state and not a property captured by a Closure. If the protected task explicitly suspends while the handle call remains active, the handler remains part of that same task’s suspended continuation and is active again when that task resumes. Other Actor-local tasks that run while it is suspended cannot observe or use that handler.

Creating a distinct asynchronous Future/task inside the protected scope does not copy or inherit the handler into that task. An unhandled failure in such a task fails its Future according to the Future rules. If a consumer later observes that failed Future through value(), the recorded error is re-signaled in the consumer’s then-current dynamic handler context. Actor boundaries likewise never carry dynamic handlers.

This protocol adds no try, catch, throw, or finally syntax and no second handler type system. User and library error prototypes use ordinary delegation, and richer handling abstractions may be built from this single primitive dynamic-scope mechanism. An unhandled error propagates until an appropriate handler is found or the outermost execution boundary is reached.

Core v0.1 retains no resumable-condition authority or continuation state for error signaling. A future standard may add an explicit recovery/restart facility only through its own normative control-state contract; such a facility must not reinterpret existing Core Error.signal() operations as resumable.

Core v0.1 defines one mandatory root error prototype: Error. Error is an ordinary standard-prelude object whose delegation parent is Object.

Every object signaled as a Core language/runtime error must have Error in its delegation chain. Standard error prototypes named normatively by Core or a normative domain model are ordinary objects in that chain. Their portable parent relations are fixed below; implementations must not insert additional Protos-visible ancestors between those prototypes.

The standard prototypes required by Core v0.1 are:

Standard prototype Immediate parent Trigger/consequence owner
Error Object this document
SlotNotFound Error EXECUTION_AND_CONTROL.md / grammar lookup rules
InvalidSuper Error EXECUTION_AND_CONTROL.md §8
InvalidReturn Error CALLABLES.md
Cancelled Error ../concurrency/FUTURES_AND_TASKS.md
FutureResolutionCycle Error ../concurrency/FUTURES_AND_TASKS.md
RequestOutcomeUncertain Error ../concurrency/ACTORS.md
NonTransferableValue Error ../concurrency/ACTORS.md
NonParallelValue Error ../concurrency/PARALLEL_EXECUTION.md
ForeignError Error VALUES_AND_COLLECTIONS.md (Foreign Values)
InvalidPredicateResult Error Standard Library predicate/comparator contracts (std:collections/Array); ../concurrency/PARALLEL_EXECUTION.md §71.6
InvalidComparatorResult Error Standard Library predicate/comparator contracts (std:collections/Array); ../concurrency/PARALLEL_EXECUTION.md §71.6
InvalidComparatorOrder Error Standard Library predicate/comparator contracts (std:collections/Array); ../concurrency/PARALLEL_EXECUTION.md §71.6
IOError Error ../io/IO_CORE.md and the modular I/O documents
InvalidIOArgument IOError ../io/IO_CORE.md
IOLifecycleError IOError ../io/IO_CORE.md
IOCapacityExhausted IOError ../io/IO_CORE.md / ../io/BYTE_IO.md
EncodingError IOError ../io/TEXT_IO.md / ../io/IO_CORE.md
LineTooLong IOError ../io/TEXT_IO.md / ../io/IO_CORE.md

This table owns the existence and parent relation of those standard Error prototype bindings. The referenced domain document owns when the corresponding category is used and any domain-specific effect, cancellation, commitment, or lifecycle consequence. A domain document need not and must not redefine the identity/construction rule for standard failure instances.

The list is deliberately minimal. In particular, Core v0.1 does not promise separate standard prototypes merely for ordinary assignment/creation failure, arity/binding failure, frozen/closed object mutation, index/range failure, Map lookup/update failure, filesystem target absence/existence/permission, native path failure, or generic backend failure unless another normative rule names one of the tabled categories. Where those rules say only Error, portable code may match only Error (or a user-defined ancestor it deliberately introduced).

This rule makes the portable taxonomy deliberately shallow. A specification may introduce a deeper standard hierarchy only by stating that hierarchy normatively; an implementation must not invent extra Protos-visible intermediate error categories. Such an invented ancestor would change handler matching and reflection and is therefore observable language behavior, not an implementation detail.

When a normative rule says only that an operation “signals an error” and does not name a standard error prototype, Core v0.1 guarantees only the Error category for portable handler matching and the fresh-instance rule above. An implementation may attach implementation-private diagnostic metadata, but it must not expose a different Protos delegation ancestry for that failure as though the additional category were standardized.

Conversely, user code and libraries may create ordinary error prototypes and arbitrary deeper delegation hierarchies beneath Error. Those program-defined hierarchies use the normal object/delegation model and handler matching rules; they do not extend the set of standard Core error categories.

A prototype name appearing only as pseudocode notation is not thereby a standard-prelude binding. A name becomes a portable standard error prototype only when a normative specification explicitly defines it as such and this taxonomy (or a later revision of its owning contract) records its parent relation.

This taxonomy rule does not introduce checked errors, declarations, hidden classes, or a parallel type system. It exists solely to make the already observable prototype-based handler matching deterministic across independent implementations.

If execution enters cleanup because a control transfer is leaving the protected scope, normal completion of cleanup preserves that pending transfer.

If cleanup instead signals an Error, the cleanup Error becomes the active error transfer and supersedes the transfer that caused cleanup to run. This applies when the prior transfer was normal scope exit, non-local return, Error unwind, or cancellation unwind.

Therefore, when an Error original is already unwinding through an ensure scope and the cleanup signals cleanupError, outward handler search observes cleanupError, not original.

Core v0.1 does not automatically wrap cleanupError, attach original as a language-visible cause, construct a suppressed-error list, or otherwise preserve both failures as a new composite Error. Libraries may build such reporting conventions explicitly with ordinary objects and handlers.

This rule does not undo effects already performed before either Error was signaled. It fixes only which control transfer continues after the cleanup attempt.