Errors as Values
Dewy represents an expected failure as an ordinary value. A function lists its successful result and its possible errors directly in one union:
let loadCustomer = (id:CustomerId)
:> Customer | NotFoundError | DatabaseError
=> {
# ...
}
Calling loadCustomer produces one of those three values. There is no Result container to unwrap and no Ok or Err constructor around either outcome. Error types belong to Dewy's nominal error family, which lets the language distinguish failures from ordinary domain alternatives.
Public functions should normally state a stable set of errors in their return contract. A private helper may allow the compiler to infer them.
Dewy never traps. A running Dewy program has no hidden exits: it stops only where you wrote
return, an explicit exit, or a$runtime_assertof your own. Whatever could fail is either proven safe at compile time (and compiles to nothing) or comes back to you as a value in the type —rational | Overflow,T | none, your own error types — for you to handle. There is no third option, in your code or in the library — and if you write a library yourself, the same courtesy applies: don't exit on your callers' behalf. Ask for the proof in a parameter's type, or hand the failure back in the result.
Exception Values Forward
Safe navigation is governed by a broader exception type family. Both error and none descend from exception, and programmers can define other exception types. Any alternative in this family forwards through navigation.
Here, “exception” describes an ordinary value's type. It does not imply throwing, catching, or stack unwinding.
Defining Exceptions
type of Parent creates a fresh nominal child. A unit-like error has one canonical inhabitant written with the type's own name:
const MyCustomError:type = type of error
let maybeNumber = ():>int64 | MyCustomError => {
if random.coinflip
return MyCustomError
return 42
}
There is currently no separate MyCustomError() spelling. Whether the canonical inhabitant and its type value are literally the same semantic object is left open.
An error that carries context combines its fresh nominal identity with a structural type:
const MyComplexError:type =
(type of error) & [extra:string fields:int64]
let problem = MyComplexError[
extra='some extra context'
fields=42
]
Only type of error creates identity. A later alias such as MyComplexError & [metadata:string] adds a structural requirement while remaining the same nominal error kind.
Safe Navigation
When a receiver might be an exception, Dewy applies a member operation to every ordinary alternative and forwards the exception unchanged:
let DatabaseError:type = type of error
let Address:type = [city:string]
let Profile:type = [address:Address|none]
let Customer:type = [profile:Profile]
let find_customer = (id:int64):>Customer | DatabaseError | none =>
if id >? 0 [profile=[address=[city="paris"]]] else DatabaseError
let city_of = (id:int64):>string | DatabaseError | none => {
let customer = find_customer(id)
let city = customer.profile.address.city
return city # string | DatabaseError | none
}
If customer is a Customer, the route reads its profile, address, and city. If it is a DatabaseError or none, none of those accesses run; that exception becomes the value of city. Every later access in the route follows the same rule, so Dewy does not need a separate ?. operator.
This behavior is type checked rather than based on whether a value happens to be truthy. Every non-exception alternative must support the requested member:
let subject:Customer | Organization | DatabaseError = findSubject(id)
let name = subject.name
This is valid only if both Customer and Organization have a usable name. An ordinary union member is never silently skipped.
If a program needs a sentinel that does not forward, it defines an ordinary type that does not descend from exception. Such a sentinel must be narrowed explicitly before accessing members that it does not support.
Propagating an Exception
Use or_throw when the current function should pass an exception back to its caller:
let loadGreeting = (id:CustomerId)
:> string | NotFoundError | DatabaseError
=> {
let customer = loadCustomer(id) or_throw
return "Hello, {customer.name}!"
}
The expression evaluates loadCustomer(id) once. A Customer becomes the local customer; an exception returns immediately from loadGreeting. The enclosing return contract must accept every exception alternative that can be forwarded this way. This applies to none and user-defined exceptions as well as errors.
Unlike navigation on a receiver, arguments do not forward implicitly:
let amount:Money | ParseError = parseMoney(text)
invoice.setAmount(amount) # type error
invoice.setAmount(amount or_throw) # passes Money or returns ParseError
Keeping arguments explicit prevents a call from acquiring hidden early exits for any exception-bearing expression supplied to it.
Inspecting and Recovering
An error is still a value, so ordinary type tests can narrow it:
let customer = loadCustomer(id)
if customer is? NotFoundError
return guestCustomer
else if customer is? DatabaseError
return customer
printl"Welcome back, {customer.name}!"
After both error alternatives have left the flow, customer is known to be a Customer. General pattern selection and concise type-directed recovery helpers are planned, but their final syntax is not yet fixed.
Not every alternative that describes an unsuccessful search should be an exception. User | Missing contains two ordinary domain outcomes and does not gain forwarding. User | NotFoundError says that the second alternative is an error intended to participate in propagation.
What the Compiler Does Today
The current compiler implements the core of this chapter: unit-like error types minted with type of error, errors as ordinary union alternatives, is? to handle them (is? error covers the whole family), and or_throw to propagate. Errors carrying fields and forwarding member access are still design.
let NotFound:type = type of error
let Invalid:type = type of error
let lookup = (id:int64):>int64 | NotFound | Invalid => {
if id <? 0 { return Invalid }
if id >? 100 { return NotFound }
return id * 2
}
let twice = (id:int64):>int64 | NotFound | Invalid => {
let first = lookup(id) or_throw # propagate NotFound / Invalid
return lookup(first) or_throw
}
let main = ():>int64 => {
let r = twice(30)
if r is? error { return 1 } # NotFound: 60 -> 120 is out of range
return r # the ordinary alternative
}
Errors, Absence, and Effects
none represents absence. It is not an error, but it is an exception, so optional navigation forwards it automatically. This makes T | none the common option-like form: use the T normally, or carry its exceptional absence through the route. Optional Values and Narrowing covers explicit tests and fallbacks.
Errors are also separate from effects. An error appears in the returned union because it is a value the caller receives. Effects describe behavior such as I/O, blocking, or mutation even when a call succeeds.
Design boundary: Direct error unions, nominal exception creation, the
exceptionforwarding family, safe receiver navigation, explicit argument handling, and the separation of errors from effects are the intended model. Transforming an exception duringor_throw, pattern matching, recovery helpers, and extending automatic forwarding to pipelines remain provisional.
The Errors and Forwarding reference gives the exact type rules and collects the remaining open points.