Skip to content
pannous edited this page Oct 16, 2026 · 18 revisions

Warp has two ways for something to go wrong: an error value that a function hands back, and a raised error that stops the program unless a try catches it. Both carry the same Error value, so the same failed test works on both.

Errors are values

error "text" makes an Error value. A function returns it like any other value, and the caller asks failed: no try, no catch.

f(x) := if x < 0 then error "neg" else x
r = f(-1)
if r failed then 1 else 2   # 1

f(3) failed is false. r is failed is the same test. An error value nothing uses warns: if x < 0 { error "neg" } drops it, fail "neg" raises it.

The Error knows its text:

f(x) := if x < 0 then error "neg" else x
r = f(-1)
r.message   # "neg"

An Error passes through what you do with it. Arithmetic on it gives the same Error back, so a caller further up can still test it:

f(x) := if x < 0 then error "neg" else x
g(x) := f(x) + 1
r = g(-1)
r failed    # yes

or gives a fallback, ! insists on the value and stops the program with the error:

f(x) := if x < 0 then error "neg" else x
r = f(-1)
r or 0      # 0

An Error that is still the result at the end of the program is printed, and warp exits with status 1.

Raised errors

Some errors are not handed back, they are raised: raise "text" (or throw), 1/0, an index out of range, assert x > 0 else "text". A raised error stops the program at once, also inside an assignment:

print "a"
r = 10/0      # stops here: Error("divide by zero at 2:1")
print "b"     # never runs

It names where it happened, line and column, and points at the code. The position is the operation that failed, also when it runs inside a function called elsewhere:

f(x) := 10 / x; f(0)
» Error("divide by zero at 1:9")
  1 | f(x) := 10 / x; f(0)
    |         ^^

try: back to a value

A bare try catches a raised error and keeps it as its value, so failed tests it as above:

r = try 10/0
if r failed then r.message else 2   # "divide by zero"

try catches the error however deep inside the call it was raised.

try X else Y gives Y instead; Y is a value or an action:

try 1/0 else 0   # 0

try { … } catch { … } is the same with blocks, and catch e names the Error:

try { raise "boom" } catch e { e.message }   # "boom"

Python's try: … except: … works too.

finally runs whatever happens; the value of try, or its Error, stays the result:

n = 0
r = try 10/2 finally n = 7
r + n   # 12

on error of f: a handler from outside

A handler attached to a function from outside: every call of the function that fails gives the handler's value, as if its body were try body else handler.

divide(x) := 10/x
on error of divide { 0 }
divide(0) + divide(5)   # 2

on error of f: 0 and on error of f do 0 are the same.

Errors are not signals

emit, on and the other signal words are a separate mechanism: emit alarm runs the on alarm handlers and the program goes on. raise and throw are always errors: an on handler never receives them, only try does.

Rust crates and components

A function of a cargo crate or a WebAssembly component that returns a Rust Err (a WIT result error) raises it in warp, with the crate's message:

use cargo semver
semver.Version.parse("x")
» Error("… unexpected character 'x' while parsing major version number")

try catches it like any other error: try semver.Version.parse("x") else 0 is 0.

Failing functions

effects of f shows when f can fail: the effect Fail (effects). try removes it:

f(x) := 10/x
g(x) := try f(x) else 0
effects of f   # Fail
effects of g   # Allocation

The type of a function that can fail is T or error, a union like int or text; warp never shows a Result type. T? is a T, ø or an Error:

def check(x) as int or error := if x < 0 then error("negative") else x
r = check(-1)
r failed       # yes
int? n = ø

Data holds no error

Results, variables and arguments carry an Error; a list does not. An Error written into a list stops the program with its own message:

e = try 10/0
[e 2]   # divide by zero: a list item holds no error; handle it with try … else, or declare `or error`
[(try e else 0) 2]   # [0 2]

Not yet (card error-gaps)

  • Only the bytes at a wit export would be a WIT result for the other side; not done yet: inferring or error, and the WIT export.
  • An Error written by xs#1 = e, into a field, a map or a tuple is still kept (cards writing-error, tuple-literal).
  • Where a caller can take it, an error could travel back as the Error value instead of being raised.
  • catch blocks inside a function, catching for the whole function (card catch-blocks):
fun do_dirty_stuff(){
 eat ()
 drink ()
 catch (no food){}
 catch (drunkenness){}
}

Decided, not built yet (card error-gaps): int? means an int, nothing (ø) or an Error, int or error stays the precise form. An Error never lands in data: writing one into a field or a list element raises ("a field holds no error: handle it or declare or error"). x is ø asks for missing only, x failed for failed only, if no x and x or 0 cover both. parse string as int? would return an int or an Error, int? i = try parse(s) would need no catch, and parse string as int may fail would mean the same as int?. A function without ? that calls a failing one would itself be marked as failing (the Fail effect above), so errors never color functions by hand. no result would be true for both missing and failed values (optional).

Home

Philosophy •

data & code blocks

features

inventions

evaluation

reflection

W0 types, proved

keywords

standard library

use modules & cargo crates, script mode

iteration

switch & pattern matching

warnings & strict

tasks

time & units

html pages

the warp command

examples

todo : bad ideas and open questions

⚠️ specification and progress are out of sync

Clone this wiki locally