Created
June 4, 2026 13:22
-
-
Save tjdevries/f0269f0b124c4c0fa3287753b735a68f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ## highlight: ship | |
| Error propagation example checks production, propagation, partial handling, residual handling, and sealing. | |
| $ ship check --show-types bin/main.shp | |
| type ConfigErrors = [ :missing_env(String) | :invalid_port(String) | :invalid_region(String) ] | |
| type OrderErrors = [ :order_not_found(String) | :db_timeout(Int) | :db_corrupt(String) ] | |
| type PaymentErrors = [ :card_declined(String) | :gateway_timeout(Int) | :fraud_hold(String) ] | |
| let read_env = fn(name: String) -> String? [ :missing_env(String) ] { | |
| // ^^^^^^^^ fn(String) -> String? [ :missing_env(String) ] | |
| // ^^^^ String | |
| if (name == "PROTECTED") { | |
| // ^^^^ String | |
| // ^^^^^^^^^^^ String | |
| error :missing_env(name); | |
| // ^^^^ String | |
| } | |
| return "5432"; | |
| // ^^^^^^ String | |
| } | |
| let parse_port = fn(raw: String) -> Int? [ :invalid_port(String) ] { | |
| // ^^^^^^^^^^ fn(String) -> Int? [ :invalid_port(String) ] | |
| // ^^^ String | |
| return match Int.parse(raw) { | |
| // ^^^^^^^^^ fn(String) -> Int? [> :parse(String) ] | |
| // ^^^ String | |
| | value -> value | |
| // ^^^^^ Int | |
| // ^^^^^ Int | |
| | error e -> { error :invalid_port("${e}"); } | |
| // ^ [> :parse(String) ] | |
| // ^ [> :parse(String) ] | |
| } | |
| } | |
| let parse_region = fn(raw: String) -> String? [ :invalid_region(String) ] { | |
| // ^^^^^^^^^^^^ fn(String) -> String? [ :invalid_region(String) ] | |
| // ^^^ String | |
| if (raw == "us-east-1") { | |
| // ^^^ String | |
| // ^^^^^^^^^^^ String | |
| error :invalid_region(raw); | |
| // ^^^ String | |
| } | |
| return "us-east"; | |
| // ^^^^^^^^^ String | |
| } | |
| let load_config = fn() -> String? [ ConfigErrors ] { | |
| // ^^^^^^^^^^^ fn() -> String? [ ConfigErrors ] | |
| let port_text = read_env("DATABASE_PORT"); | |
| // ^^^^^^^^^ String | |
| // ^^^^^^^^ fn(String) -> String? [ :missing_env(String) ] | |
| // ^^^^^^^^^^^^^^^ String | |
| let port = parse_port(port_text); | |
| // ^^^^ Int | |
| // ^^^^^^^^^^ fn(String) -> Int? [ :invalid_port(String) ] | |
| // ^^^^^^^^^ String | |
| let region = parse_region("us-east"); | |
| // ^^^^^^ String | |
| // ^^^^^^^^^^^^ fn(String) -> String? [ :invalid_region(String) ] | |
| // ^^^^^^^^^ String | |
| return "${region}:${port}"; | |
| // ^^^^^^ String | |
| // ^^^^ Int | |
| } | |
| let load_config_or_default = fn() { | |
| // ^^^^^^^^^^^^^^^^^^^^^^ fn() -> String? [ ConfigErrors - :missing_env ] | |
| return try load_config() { | |
| // ^^^^^^^^^^^ fn() -> String? [ ConfigErrors ] | |
| | :missing_env(name) -> "local:5432" | |
| // ^^^^ String | |
| // ^^^^^^^^^^^^ String | |
| }; | |
| } | |
| let load_config_or_message = fn() { | |
| // ^^^^^^^^^^^^^^^^^^^^^^ fn() -> String | |
| return try load_config_or_default() { | |
| // ^^^^^^^^^^^^^^^^^^^^^^ fn() -> String? [ ConfigErrors - :missing_env ] | |
| | rest -> "config unavailable" | |
| // ^^^^ [ ConfigErrors - :missing_env ] | |
| // ^^^^^^^^^^^^^^^^^^^^ String | |
| }; | |
| } | |
| let lookup_order = fn(order_id: String) -> String? [ OrderErrors ] { | |
| // ^^^^^^^^^^^^ fn(String) -> String? [ OrderErrors ] | |
| // ^^^^^^^^ String | |
| error :order_not_found(order_id); | |
| // ^^^^^^^^ String | |
| error :db_timeout(30); | |
| // ^^ Int | |
| error :db_corrupt("orders"); | |
| // ^^^^^^^^ String | |
| return "order:${order_id}"; | |
| // ^^^^^^^^ String | |
| } | |
| let render_order_summary = fn(order_id: String) { | |
| // ^^^^^^^^^^^^^^^^^^^^ fn(String) -> String? [ OrderErrors - :order_not_found ] | |
| // ^^^^^^^^ String | |
| return match lookup_order(order_id) { | |
| // ^^^^^^^^^^^^ fn(String) -> String? [ OrderErrors ] | |
| // ^^^^^^^^ String | |
| | order -> "loaded ${order}" | |
| // ^^^^^ String | |
| // ^^^^^ String | |
| | error :order_not_found(missing_id) -> "missing order ${missing_id}" | |
| // ^^^^^^^^^^ String | |
| // ^^^^^^^^^^ String | |
| }; | |
| } | |
| let render_order_or_queue = fn(order_id: String) { | |
| // ^^^^^^^^^^^^^^^^^^^^^ fn(String) -> String | |
| // ^^^^^^^^ String | |
| return try render_order_summary(order_id) { | |
| // ^^^^^^^^^^^^^^^^^^^^ fn(String) -> String? [ OrderErrors - :order_not_found ] | |
| // ^^^^^^^^ String | |
| | rest -> "order queued for review" | |
| // ^^^^ [ OrderErrors - :order_not_found ] | |
| // ^^^^^^^^^^^^^^^^^^^^^^^^^ String | |
| }; | |
| } | |
| let charge_card = fn(order: String) -> String? [ PaymentErrors ] { | |
| // ^^^^^^^^^^^ fn(String) -> String? [ PaymentErrors ] | |
| // ^^^^^ String | |
| return match order { | |
| // ^^^^^ String | |
| | "slow" -> { error :gateway_timeout(30); } | |
| // ^^ Int | |
| | "broke" -> { error :card_declined(order); } | |
| // ^^^^^ String | |
| | "stealing" -> { error :fraud_hold(order); } | |
| // ^^^^^ String | |
| | order -> "receipt:${order}" | |
| // ^^^^^ String | |
| // ^^^^^ String | |
| }; | |
| } | |
| let checkout = fn(order_id: String) { | |
| // ^^^^^^^^ fn(String) -> String? [ :db_corrupt(String) | :db_timeout(Int) | :fraud_hold(String) | :gateway_timeout(Int) | :order_not_found(String) ] | |
| // ^^^^^^^^ String | |
| let order = lookup_order(order_id); | |
| // ^^^^^ String | |
| // ^^^^^^^^^^^^ fn(String) -> String? [ OrderErrors ] | |
| // ^^^^^^^^ String | |
| return try charge_card(order) { | |
| // ^^^^^^^^^^^ fn(String) -> String? [ PaymentErrors ] | |
| // ^^^^^ String | |
| | :card_declined(declined_order) -> "ask for another card" | |
| // ^^^^^^^^^^^^^^ String | |
| // ^^^^^^^^^^^^^^^^^^^^^^ String | |
| }; | |
| } | |
| let checkout_or_ticket = fn(order_id: String) { | |
| // ^^^^^^^^^^^^^^^^^^ fn(String) -> String | |
| // ^^^^^^^^ String | |
| return try checkout(order_id) { | |
| // ^^^^^^^^ fn(String) -> String? [ :db_corrupt(String) | :db_timeout(Int) | :fraud_hold(String) | :gateway_timeout(Int) | :order_not_found(String) ] | |
| // ^^^^^^^^ String | |
| | rest -> "support ticket" | |
| // ^^^^ [ :db_corrupt(String) | :db_timeout(Int) | :fraud_hold(String) | :gateway_timeout(Int) | :order_not_found(String) ] | |
| // ^^^^^^^^^^^^^^^^ String | |
| }; | |
| } | |
| let health = fn() -> String! { | |
| // ^^^^^^ fn() -> String! | |
| return "ok"; | |
| // ^^^^ String | |
| } | |
| let main = fn() { | |
| // ^^^^ fn() -> Unit | |
| print(load_config_or_message()); | |
| // ^^^^^ fn(...'a, ~sep: String, ~end: String) -> Unit | |
| // ^^^^^^^^^^^^^^^^^^^^^^ fn() -> String | |
| print(render_order_or_queue("order-404")); | |
| // ^^^^^ fn(...'a, ~sep: String, ~end: String) -> Unit | |
| // ^^^^^^^^^^^^^^^^^^^^^ fn(String) -> String | |
| // ^^^^^^^^^^^ String | |
| print(checkout_or_ticket("order-500")); | |
| // ^^^^^ fn(...'a, ~sep: String, ~end: String) -> Unit | |
| // ^^^^^^^^^^^^^^^^^^ fn(String) -> String | |
| // ^^^^^^^^^^^ String | |
| print(health()); | |
| // ^^^^^ fn(...'a, ~sep: String, ~end: String) -> Unit | |
| // ^^^^^^ fn() -> String! | |
| } | |
| The same example runs with expected errors short-circuiting through calls and handlers. | |
| $ ship run . | |
| us-east:5432 | |
| missing order order-404 | |
| support ticket | |
| ok |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment