<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jayson Reis</title>
    <description>The latest articles on DEV Community by Jayson Reis (@jaysonsantos).</description>
    <link>https://dev.to/jaysonsantos</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F582713%2F3ef35bef-fe37-4edb-b412-023fb0e176cc.jpeg</url>
      <title>DEV Community: Jayson Reis</title>
      <link>https://dev.to/jaysonsantos</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jaysonsantos"/>
    <language>en</language>
    <item>
      <title>Beni: a typed functional language for the browser, with no async/await</title>
      <dc:creator>Jayson Reis</dc:creator>
      <pubDate>Fri, 16 Oct 2026 18:37:04 +0000</pubDate>
      <link>https://dev.to/jaysonsantos/beni-a-typed-functional-language-for-the-browser-with-no-asyncawait-4ijc</link>
      <guid>https://dev.to/jaysonsantos/beni-a-typed-functional-language-for-the-browser-with-no-asyncawait-4ijc</guid>
      <description>&lt;p&gt;Beni is a statically typed functional language that compiles to JavaScript. My friend Cesar (&lt;a href="https://x.com/ccanassa" rel="noopener noreferrer"&gt;@canassa&lt;/a&gt;) started it, and I've been working on it with him. It's pre-1.0, but it's far enough along that we'd like people to try it.&lt;/p&gt;

&lt;p&gt;You can try it in the &lt;a href="https://canassa.github.io/beni/playground/" rel="noopener noreferrer"&gt;playground&lt;/a&gt; without installing anything. The compiler runs in the browser as WebAssembly. The code is at &lt;a href="https://github.com/canassa/beni" rel="noopener noreferrer"&gt;github.com/canassa/beni&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Beni?
&lt;/h2&gt;

&lt;p&gt;Elm showed that a web app can run for years without a runtime error. Most web apps are written in TypeScript instead, and its types don't always mean what they say.&lt;/p&gt;

&lt;p&gt;Beni started as an Elm clone for the browser and turned into an experiment in how far current tools can be pushed, borrowing from Roc, Solid, Effect and Zig. It has three goals, and the rest of this post takes them one at a time.&lt;/p&gt;

&lt;h3&gt;
  
  
  TypeScript's types can lie
&lt;/h3&gt;

&lt;p&gt;TypeScript doesn't try to be sound. It's &lt;a href="https://github.com/microsoft/TypeScript/wiki/TypeScript-Design-Goals" rel="noopener noreferrer"&gt;listed as a non-goal&lt;/a&gt;, and it's easy to end up with a &lt;code&gt;string[]&lt;/code&gt; that holds a number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ada&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grace&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt; &lt;span class="c1"&gt;// allowed&lt;/span&gt;
&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="c1"&gt;// compiles, then throws on 42&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This compiles without a warning, because arrays are covariant in TypeScript. And when the checker does complain, it's easy to switch it off with &lt;code&gt;as&lt;/code&gt; or &lt;code&gt;any&lt;/code&gt;. That matters more now that agents write a lot of code unattended: at the first sign of a type error, an agent will often reach for &lt;code&gt;as any&lt;/code&gt; and move on. Errors aren't in the types either. Nothing in a function's signature says it can throw, so the compiler can't tell you which failures you haven't handled.&lt;/p&gt;

&lt;p&gt;Beni has no &lt;code&gt;any&lt;/code&gt;, no casts and no subtyping, so a &lt;code&gt;List String&lt;/code&gt; only ever holds strings, and there's no escape hatch for a person or an agent to reach for. Failures are values in the type, like the &lt;code&gt;Result&lt;/code&gt; that &lt;code&gt;fetchName&lt;/code&gt; returns further down. Data from outside, like JSON, comes in through a schema that checks it. And because Beni uses Hindley-Milner inference, the compiler works out the types, so you get all this without writing annotations everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Programs that don't crash
&lt;/h2&gt;

&lt;p&gt;The first goal is the one Elm already reached: if a program compiles, it shouldn't crash. Beni keeps that rule and tightens it where your code meets the browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  Runtime errors
&lt;/h3&gt;

&lt;p&gt;Beni has no &lt;code&gt;null&lt;/code&gt;, &lt;code&gt;undefined&lt;/code&gt; or exceptions, and a &lt;code&gt;case&lt;/code&gt; has to cover every variant. If you add a variant, the compiler lists every place that needs updating. This works the same as in Elm.&lt;/p&gt;

&lt;p&gt;Beni code can't call JavaScript directly. Browser and Node APIs are wrapped by hand in the platform layer, and each failure an API documents is returned as a typed error such as &lt;code&gt;QuotaExceeded&lt;/code&gt; or &lt;code&gt;NotFound&lt;/code&gt;. Catch-all &lt;code&gt;try&lt;/code&gt;/&lt;code&gt;catch&lt;/code&gt; isn't allowed there. If an API fails in a way its docs don't mention, we treat it as a bug in Beni, and the program stops instead of continuing in a bad state.&lt;/p&gt;

&lt;p&gt;So the guarantee covers the code you write. The platform layer is written and reviewed by hand, and the compiler doesn't verify it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error messages
&lt;/h3&gt;

&lt;p&gt;Error messages get the same attention as in Elm, and almost every one has a test that pins its wording. If you pass &lt;code&gt;foldl&lt;/code&gt; a lambda that takes one argument instead of two, you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I need a function of 2 arguments, and this one takes 1.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With currying, the compiler couldn't give that message, which is one of the reasons Beni dropped it.&lt;/p&gt;

&lt;h3&gt;
  
  
  No automatic currying
&lt;/h3&gt;

&lt;p&gt;In a curried language, a lambda with a missing parameter often still type checks, because the compiler assumes you meant to return a function. The error shows up later, usually far from the mistake. Without currying, a one-argument function and a two-argument function have different types, and the error points at the lambda.&lt;/p&gt;

&lt;p&gt;Partial application is written with &lt;code&gt;_&lt;/code&gt;, as in &lt;code&gt;List.map xs (scale 2 _)&lt;/code&gt;. The &lt;code&gt;_&lt;/code&gt; can stand for any argument, including the first. What you lose is point-free style.&lt;/p&gt;

&lt;h2&gt;
  
  
  Output as small as hand-written JavaScript
&lt;/h2&gt;

&lt;p&gt;The second goal is output that's about as small and fast as JavaScript you'd write by hand.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compilers are a good thing
&lt;/h3&gt;

&lt;p&gt;A TypeScript project usually needs a type checker, a bundler, a minifier, a formatter and a dev server. Beni is one binary that does all of it.&lt;/p&gt;

&lt;p&gt;We own the language as well as the compiler, so the compiler knows much more about the code than a JavaScript bundler does. It has the types, and it infers which functions are pure. That opens up optimisations a bundler can't safely do: removing code nothing uses, inlining functions, reordering and rewriting them to get the smallest bundle possible. Some of this is built, and a lot of it is still research.&lt;/p&gt;

&lt;p&gt;Compilers for C and Haskell have done this kind of whole-program optimisation for decades. We want to see how much of it works for code that ships to a browser.&lt;/p&gt;

&lt;h3&gt;
  
  
  Immutable lists without a trie
&lt;/h3&gt;

&lt;p&gt;Every value in Beni is immutable, lists included. The usual way to make immutable lists fast is a persistent data structure like a trie, but in the browser that's more code to download: 1 to 1.5 kB with brotli, even for a minimal one.&lt;/p&gt;

&lt;p&gt;Beni uses plain JavaScript arrays instead. An ownership checker, similar to Rust's borrow checker but with no annotations, works out when nothing else can see a list, and then the update happens in place. When it can't prove that, you get a compile error and add a &lt;code&gt;List.copy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Its core rules are modelled in Lean 4. All that to save a kilobyte and a few nanoseconds is a bit crazy, and we know it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A language that gets out of your way
&lt;/h2&gt;

&lt;p&gt;The third goal is that safety shouldn't come with ceremony, and that the compiler should be fast enough that you never wait on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  No async/await
&lt;/h3&gt;

&lt;p&gt;In TypeScript, once a function awaits something, every function that calls it has to become async too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// greet has to be async now, and so does everything that calls it&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Hello, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello, stranger&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bob Nystrom called this function coloring in &lt;a href="https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/" rel="noopener noreferrer"&gt;What Color Is Your Function?&lt;/a&gt;. In Beni the same code looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elm"&gt;&lt;code&gt;&lt;span class="n"&gt;fetchName&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="kt"&gt;Result&lt;/span&gt; &lt;span class="kt"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Error&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
&lt;span class="n"&gt;fetchName&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/users/${String.fromInt id}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expectString&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="n"&gt;greet&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
&lt;span class="n"&gt;greet&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;fetchName&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt;
        &lt;span class="kt"&gt;Ok&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt;
            &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello, ${name}"&lt;/span&gt;

        &lt;span class="kt"&gt;Err&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt;
            &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello, stranger"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fetchName&lt;/code&gt; waits on the network, and &lt;code&gt;greet&lt;/code&gt; calls it like any other function. An HTTP failure comes back as a &lt;code&gt;Result&lt;/code&gt;, so the compiler makes you handle it.&lt;/p&gt;

&lt;p&gt;The compiler works out which functions might wait. Each function gets two flags, whether it can suspend and whether it has side effects, and you don't write them anywhere. Functions that can suspend are compiled to run on a small fiber runtime that ships with the program. Functions that can't are compiled to plain JavaScript. Higher-order functions only need one version: &lt;code&gt;List.map&lt;/code&gt; takes a callback that makes network requests the same way it takes one that doesn't.&lt;/p&gt;

&lt;p&gt;The runtime is modeled on &lt;a href="https://effect.website/" rel="noopener noreferrer"&gt;Effect&lt;/a&gt;, so timeouts, retries, cancellation and concurrency limits are library functions. This example fetches up to 4 pages at a time, retries each one with exponential backoff, and stops after 10 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elm"&gt;&lt;code&gt;&lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Schedule&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Schedule&lt;/span&gt; &lt;span class="kt"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Error&lt;/span&gt;
&lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Schedule&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exponential&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;millis&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt; &lt;span class="err"&gt;▷&lt;/span&gt; &lt;span class="kt"&gt;Schedule&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upTo&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;loadAll&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="kt"&gt;Maybe&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Result&lt;/span&gt; &lt;span class="kt"&gt;Http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;loadAll&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;λ⊤&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt;
    &lt;span class="kt"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;forEachOk&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="err"&gt;λ&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="kt"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retry&lt;/span&gt; &lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="err"&gt;λ⊤&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="n"&gt;fetchPage&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A cancelled fiber always runs its cleanup. The runtime with these pieces is about 2.1 kB, compared to about 27 kB for Effect.&lt;/p&gt;

&lt;p&gt;The downside is that the source doesn't show where a function might suspend. Where that matters, a &lt;code&gt;sync&lt;/code&gt; block tells the compiler the code inside must never suspend, and the compiler checks everything that code calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Static dispatch
&lt;/h3&gt;

&lt;p&gt;Beni has static dispatch, modeled on Roc's. A type's methods are the public functions of the module that defines it, so &lt;code&gt;v.add w&lt;/code&gt; calls &lt;code&gt;Vec.add v w&lt;/code&gt;, resolved at compile time. Generic code can ask for a method with a &lt;code&gt;where a.add : a, a → a&lt;/code&gt; clause. &lt;code&gt;==&lt;/code&gt; and &lt;code&gt;&amp;lt;&lt;/code&gt; call the type's &lt;code&gt;eq&lt;/code&gt; and &lt;code&gt;compare&lt;/code&gt;, which records and custom types get automatically.&lt;/p&gt;

&lt;p&gt;This fixes two things that come up a lot in Elm. Custom types can be &lt;code&gt;Dict&lt;/code&gt; keys and can be sorted, without Elm's special &lt;code&gt;comparable&lt;/code&gt; type. And a method can be picked by the type you expect back, which is how JSON decoding works: &lt;code&gt;Json.decode text&lt;/code&gt; finds the decoder from the result type, so you don't write one by hand for every record.&lt;/p&gt;

&lt;p&gt;You get most of what type classes give you, without instances, orphan rules or the compile-time cost that comes with them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unicode syntax
&lt;/h3&gt;

&lt;p&gt;Maybe the most controversial decision 😅&lt;/p&gt;

&lt;p&gt;We use Unicode symbols where we think they make code easier to read. It's 2026, and editors and terminals render them fine.&lt;/p&gt;

&lt;p&gt;Also, we're having fun. What's the point of writing your own language if it looks like every other C clone?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Haskell writes a lambda as &lt;code&gt;\x -&amp;gt; …&lt;/code&gt; because the backslash looks a bit like a λ. Beni just uses &lt;code&gt;λx → …&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Tuple types are written &lt;code&gt;Int × String&lt;/code&gt;. Function types already use commas for multiple arguments, so &lt;code&gt;Int × Int → Int&lt;/code&gt; takes one pair and &lt;code&gt;Int, Int → Int&lt;/code&gt; takes two arguments.&lt;/li&gt;
&lt;li&gt;Functions that return nothing useful get their own symbol, &lt;code&gt;⊤&lt;/code&gt;, as in &lt;code&gt;String → ⊤&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The pipe is &lt;code&gt;▷&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the hardcore folks still typing code by hand: write &lt;code&gt;-&amp;gt;&lt;/code&gt; and &lt;code&gt;beni fmt --migrate-unicode&lt;/code&gt; turns it into &lt;code&gt;→&lt;/code&gt; for you. Editor plugins will do the same as you type, once they exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Batteries included
&lt;/h2&gt;

&lt;h3&gt;
  
  
  JSX
&lt;/h3&gt;

&lt;p&gt;Markup is part of the language and is type checked against the elements the platform defines. It compiles the way Solid does: each template is cloned from HTML once, and only the parts that depend on changed values get updated. There's no virtual DOM. Records are immutable and unchanged fields keep their identity, so one reference comparison per dynamic part is enough to tell whether it changed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elm"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;Html&lt;/span&gt; &lt;span class="k"&gt;exposing&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;Tea&lt;/span&gt;


&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="kt"&gt;Msg&lt;/span&gt;
    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Increment&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kt"&gt;Decrement&lt;/span&gt;


&lt;span class="n"&gt;update&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Msg&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;
&lt;span class="n"&gt;update&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt;
        &lt;span class="kt"&gt;Increment&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt;
            &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

        &lt;span class="kt"&gt;Decrement&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt;
            &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;


&lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="kt"&gt;Html&lt;/span&gt; &lt;span class="kt"&gt;Msg&lt;/span&gt;
&lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt; &lt;span class="n"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kt"&gt;Decrement&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;-&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt; &lt;span class="n"&gt;onClick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kt"&gt;Increment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;+&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;button&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;


&lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Tea&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="kt"&gt;Program&lt;/span&gt;
&lt;span class="n"&gt;main&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Tea&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sandbox&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;init&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;λ⊤&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;view&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://canassa.github.io/beni/playground/#platform=browser-direct&amp;amp;code=dVExUsMwEOz1ii1hYsfQZmzPMFBAwQwFH1BsYTSJJY8lk2SMWwpK3sAv-AOPyEuQzrYmFFHh0-7t3e3JcYwbFLpTVrQraCWw7qzVCrwsjccR7KuAdp8Wlm8EkeA7flgyWTe6tbi39RZi32gjVYULDy_n3LPgjDF7aAQeTcXgToYHVbSiFsoSfsedmDFjXVNyK7Dy8sgpLY4fXz7OmdpUo2FkVF5wM5L6hbA_YYAvDiypqXKBaxboMP2MOPZi9ibFzrmaDdHOfiPiT_2kNZcqD33S6T21ut3KYpP1YdyQx2kyZk_kpuEq76nhkCaEzvcKew754n-vNBltMOajM-5-xPKp1VXL65HKiDJclWu9Rw-ppFsBvz_Hz29a8SrC9OTZdIlA22ZjGNgf&amp;amp;css=jVXLbus2EN37KwYuisY3ki0_EyibFt10064uChTo4lLkyCIiiQJJ20kv8j39j35ZZ0hJtuMUt1IcSEfkPM6cGS4-wecKQZpD69H-4KA25hnulJFuodDpfbvoavG6t7RAzRsF__y9me9mOXw2yvz6-8-0w792WAqJyQQAOrFna7U52ASksCoBVwllTiBaBUJKbH0CugUBtd5XPsKghH0GJytscA6fFpNJbo3x8JVMsjGbxm953PREcJqyqxy-K7d8R4gdMhSuCHl88QQtwxWh4uC9aQncPPIdwVq3bA13fEcoRktg8bguN72L0siDI0yWD-pBRSxmmEMGq-4FNvTLwO4LcZcl0P_NV7OEv2_p4zb7cMVy9jR5m0x-bFBpAXedxRKtS6_zZ6JmgZczQ-854jVPAR9JWu7ofhzAnqYVX7sB7IlCSXc5gCNVake3GOCerLVYi002gCNd-Piwe5AD_H_I2X6TnM2M7b0xQZVv6pB3IeRzFGYOR2HvYrZhYSBkQDmzgJaG41tnGSx33ctiOd_A9Besj-i1FPAbHnCawAgk8JPVoiYBi9alDq2Okjph8ax9ysZS11ARKt3ucxKyp9VaOFShjoVRryFMpR23UA57q4Ng6E1iqj02JCTmDC3DhXlJnf4rGCuMVWhTgvhLo9u0QlZ-DsssO1YBFHavqTSB_04oFTaumOAlMR1iWPTdHerthz6HAv0JsQXtqXtPBmKVXei8RlBzXoVd1hiioOT27W3Ye9FFt7dxrFcRvS0URzQbk-4VEj_Ft9lV_Bx1AsJdTJ0KBXsKMbtOxJiZqJNWvqIqy-pGCFGhoxSYbZLxQx9lgE49zassGzHarKm6aXtoSASSmBTFoRaWAceruB3OBWKEJZcGws5UsSYC0SHUPsztLjofdg_vI5FZZInlQLaJVGdqrfqE2PHsvCC1xAlPp232_XveW9PiDSExnove0G1FKfp3DGUfJykP1rGtzuhBDN5Sr2iveWb0MQWHPABdAufny_J21uytaKio03QK2tF5UL12FbZ8dhhLpkm7VOHp_TQHFLLqBctfT7SaBw1V_uCAjAtqL3rikwtMCZR1pDoJJ80zYueC7s2pDVWiPHmTk5Y6gi1ZEhbaeYjMoTS06wv991TFL1BpOhRDJwUHpTUNu0jgRLwh2SkshYR2MMS-gkT7QZoXSP4wKOCC4PXQsP2yUlvnU1npWl1t6ePIYfrnarVcTZ8-wGAB0-mlsVp8w9b9Ozv3tzYqc6QqfD0r7b_66mO8r_XYvOEQBTv075A1o-lRO13UMUZz8PGoyd7Piqw_T_gXnYXds8ughfT6GA0FXVLqTR4fa-Hxjztqp-sztxEv4_zY0UEUT9pxJPbDbtkPjIsuXfZtS7YAxnF0VePt5ZKLQTCOgs3jYHbosgG52HJVwiv7q3Hx2-Rf" rel="noopener noreferrer"&gt;Try the counter in the playground&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Schemas
&lt;/h3&gt;

&lt;p&gt;Almost every project I've worked on has needed a validation library like Zod or Joi. In Beni, schemas are part of the language. Because the compiler handles them, each schema is compiled into its own small, specialised JavaScript parser. You ship code only for the schemas you actually use, not a whole validation library.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elm"&gt;&lt;code&gt;&lt;span class="n"&gt;schema&lt;/span&gt; &lt;span class="kt"&gt;Item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Int&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
    &lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Bool&lt;/span&gt;


&lt;span class="n"&gt;firstTitle&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
&lt;span class="n"&gt;firstTitle&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="kt"&gt;Item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt;
        &lt;span class="kt"&gt;Ok&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt;
            &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;

        &lt;span class="kt"&gt;Err&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt;
            &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invalid"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://canassa.github.io/beni/playground/#platform=node&amp;amp;code=fZDNSgQxDMfvfYrQk8LugNcFDwoeFkQFvTkidSa7E22b0sYvxKsP4CP6JLaddVg_MLfkn_zzS-ZzOIDUDegMdOwCWUwgDCQJ-NFDMDFhbOCEId57IYfwYCz1Rog9WLqJJj43ilzgKLmrR8CnwIn8GnbOIq-jcbtKqc2KpaCDfQU5qIcFLL3UREgs5vxcYp6spZ59qRwy2zy_opjk4lsXfLy9fw1sybcpg40rOpOwrmzqGaPEq6qVOL3Ld2agbDTVKloZqUhqqh_FCNe_OjX5-g6dEZ0hn-E2R4_pyFHe0oQMKsfkMU0Ol7DFrV9aTX2rF7A3g1bX9SVrdRoogAwIgaQbWl3k8p2iroxN-Kony9nflq2u_T-MPQf83-5KfQI" rel="noopener noreferrer"&gt;Run this example&lt;/a&gt; in the playground. It parses one valid and one invalid JSON string.&lt;/p&gt;

&lt;p&gt;Often you don't need a schema at all. &lt;code&gt;Json.decode&lt;/code&gt; works out the type from how your code uses the result, and checks the JSON against it. If your code reads &lt;code&gt;user.name&lt;/code&gt; as a string, the JSON has to have a &lt;code&gt;name&lt;/code&gt; that's a string, and you never write a type annotation. When decoding fails, each error says whether the text wasn't JSON at all or the JSON didn't fit, along with where and why. &lt;a href="https://canassa.github.io/beni/playground/#platform=browser-direct&amp;amp;code=ZVJLTgMxDN3nFNasQKqmG1ZV0x0IKqCVCrtKKHTc1qKTjJJMPxrNlgVLzsAtuAOH6EnIZ6Yf1Zskfrbfsx3KC6Ut3Nt8BbgtlCG5gCv_vGYUsaFRsr1PZkvMRft6QcEYs7sC4cksGDjjcJuRxQwmVrtSDi6LTFiEng_pNG7Yf363EQ2emwW8AQ9FZsJEh5qHt7emrMWt9dkHvzfvdExmqUrriC45IhJyTxh8Y2mGM5VhxE7oRh9QGtQXVLFgatVrUTjYx6RS5MiOQrUGMqZEc5FMc3gkY1Mhd23I3y8FoZQGGZw3I07HQhu8E7TyTS9RnhXylpBcixVlMJyMnpMzGFcGL-OlsiCC4sQNa024OZ9V-AN-jwE6GVY_FyQHh4J9DwmNArTaGF7d1OCElMgrD9Sg5IMsSsuruLIauie5bg8OG1THjdT9buOMZN3Ixpg_nUL3ydKxVgst8ujiwWWEzN7VFiogSU6pm-T-6yc0klQwTfxSpknP3d5R0jSBOulA89d4c-lAaJXHo2b_&amp;amp;css=jZRvb5swEMbf51OcMk1KKpwCIWlK32yaNE2T2r3ovoCBA6wARsZJ2k397vPZJk36RytOJPz44rv7-XEuL-B3jdBIuQVZgjbvfcMfKyV3XQEDb_sGhxQ45NhphQXkXBUBNKKqNXATUnC1XcDF5WSSKik1_J0A5LKRig15jS2mLvbGyIz1vDLCp3JFw0m0H0n2cZLGB22kyD5OancaKWyd0XBaIzraDNc0nMRzKtOI2WZZJj7DUPNCHlIIIe4fIDHfEFSV8VkYgP8s4nlA6yuzuArfjIjmN5OnyeRLi4XgMOsVlqgGdt4qwZhbBM8wXuKgmBurH3lEazM2o-iJxPSsR9EzwdyMchRHKtf8mvN4VD2XJV_yJBzFIxncXK2v8lH-CJvVf9kkc9rvifjUum1s2xnPt85FKey5mrlmbaDlMarUmFVLSfUtwxCidf9wGS0SmP7AZo9a5BzucIfTAI5CAF-V4E1gLNoNbEAlSns-mSwebf5CDGTkFColCtrfzHJkQmNr_EwwUJGcyQc2iD-iq1LzrgpUzEi00oqO1UjmTSEKw31tRa4q0RlgdkteFPaHVLFN33LRvZ2-4v0YB3AQha5TyjBLYsM0oASf52d7xomLfQ2SDDI_1u5P0C252fxYS5pmWEqF_lqapgny9Of9r7vpq6OwdjqeBVExPopiV0aD2hBjQ89zW164CDfYPiMx1LSWbQps41nQyXKF3NnhXcyeBQE4Z-oTu2gzN9YbZCMKXy3ZfP42oA85LUqsy1awE6yVnaTOMIDp_Xe4NVNjtlvsGhnAN9mZvHwI4BhGuyh0gPaoyJDNWc9pKfPdYDuXO-1uZPyiAXclbUU-hsmyHNDUxiLP0Cz0O33uqKyR-fYEFdPyxFoertPeQ3bG5aSOk3NfevxWOvhrsArDd7wQOy9Q_4ybf_zu9I5Jw6hs5IEdFN0C3j0ealRIHf4D" rel="noopener noreferrer"&gt;This small page&lt;/a&gt; in the playground shows it: type into the box, and it shows the name in capitals, "invalid JSON" when the text can't be parsed, or "not a user" when it parses but doesn't fit.&lt;/p&gt;

&lt;p&gt;When the JSON doesn't match your types, for example renamed keys or default values, you write a &lt;code&gt;schema&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platforms
&lt;/h2&gt;

&lt;p&gt;The language itself knows nothing about where it runs. That idea comes from &lt;a href="https://www.roc-lang.org/" rel="noopener noreferrer"&gt;Roc&lt;/a&gt;. A platform supplies the program's &lt;code&gt;main&lt;/code&gt;, its effects and its bindings to JavaScript. Beni ships platforms for Node and for the browser, with or without The Elm Architecture.&lt;/p&gt;

&lt;p&gt;This is also how Beni gets out of Elm's walled garden. In Elm, your code can't call JavaScript, and ports are the only way out. In Beni, every line of JavaScript lives in a platform, wrapped so that each failure comes back as a typed value. When you need something the shipped platforms don't cover, like an npm library or a browser API, you add your own platform layer to the project. Libraries stay pure Beni, so a dependency can't break the guarantees.&lt;/p&gt;

&lt;p&gt;We want people to fork the shipped platforms and change them for their own needs. A Bun or Deno platform could expose what those runtimes offer beyond Node. A &lt;code&gt;node-static&lt;/code&gt; platform could render each route of an app into a static HTML file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apps built with Beni
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://canassa.github.io/beni/" rel="noopener noreferrer"&gt;examples site&lt;/a&gt; has three apps, each built with &lt;code&gt;--release&lt;/code&gt; and served as is, without a bundler:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Conduit, the RealWorld Medium clone, structured like elm-spa-example&lt;/li&gt;
&lt;li&gt;Bike Wars, a two-player light-cycle game ported from Elm&lt;/li&gt;
&lt;li&gt;Sternhalma, Chinese checkers for up to six players, also ported from Elm&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Current state
&lt;/h2&gt;

&lt;p&gt;Beni is pre-1.0, and the syntax still changes often. There's no package manager, editor support, user documentation or test runner yet. It's not ready for production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it
&lt;/h2&gt;

&lt;p&gt;The easiest way is the &lt;a href="https://canassa.github.io/beni/playground/" rel="noopener noreferrer"&gt;playground&lt;/a&gt;. Pick a sample such as Counter or TodoMVC, edit it, and share the link. You can add your own CSS too.&lt;/p&gt;

&lt;p&gt;To run it locally, the repo pins Zig 0.17 and Node 24 with a Nix flake (&lt;code&gt;direnv allow&lt;/code&gt; puts them on your &lt;code&gt;PATH&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/canassa/beni &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;beni
zig build
./zig-out/bin/beni new &lt;span class="nt"&gt;--platform&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;browser-tea hello
./zig-out/bin/beni serve hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;serve&lt;/code&gt; rebuilds and reloads the page when you save.&lt;/p&gt;

&lt;p&gt;If something breaks, or an error message doesn't make sense, please &lt;a href="https://github.com/canassa/beni/issues" rel="noopener noreferrer"&gt;open an issue&lt;/a&gt;. We'd especially like to hear from Elm users about the changes, removing currying in particular.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>elm</category>
      <category>functional</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Connecting to services that require jumphost from terraform</title>
      <dc:creator>Jayson Reis</dc:creator>
      <pubDate>Sun, 29 Aug 2021 20:20:03 +0000</pubDate>
      <link>https://dev.to/jaysonsantos/connecting-to-services-that-require-jumphost-from-terraform-1aog</link>
      <guid>https://dev.to/jaysonsantos/connecting-to-services-that-require-jumphost-from-terraform-1aog</guid>
      <description>&lt;p&gt;If you have services that require jumphosts to connect to them because there is no VPN, direct access, or configured zero trust mechanisms, it used to be a bit painful doing it with terraform.&lt;br&gt;
Some would use things like &lt;code&gt;null_resource&lt;/code&gt; to start the ssh command but still having a static number of tunnels.&lt;br&gt;
Thinking of that use case, I've created a terraform provider that can play well with dependencies on resources being created like databases instances or any other services that are to be provisioned and will have a non deterministic hostname.&lt;/p&gt;

&lt;p&gt;The cool thing is that the provider connects once to the jump host and you can have an arbitrary number of tunnels that connect to remote servers and assign a local port by using data sources. The provider itself only connects to the jump host when the first data source is being read, this will make sure that the connection only starts when you have a definitive hostname (i.e. an RDS instance that finished being provisioned). Once the jump host connection is done, many data sources can create tunnels using the same jump host connection.&lt;/p&gt;
&lt;h2&gt;
  
  
  how to use it
&lt;/h2&gt;

&lt;p&gt;Cutting to the chase, to use it you need to configure the provider itself with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;jumphost&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"jaysonsantos/jumphost"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0.0.2"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, configure where your jumphost is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"jumphost"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;hostname&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-server"&lt;/span&gt;
  &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ubuntu"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then configure the tunnels you are going to need to use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"jumphost_ssh"&lt;/span&gt; &lt;span class="s2"&gt;"my_database"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;hostname&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;my_database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_db_instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;my_database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Configure a postgresql provider to use the tunnel&lt;/span&gt;

&lt;span class="k"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"postgresql"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"localhost"&lt;/span&gt; &lt;span class="c1"&gt;// As this will be tunneled, just use localhost&lt;/span&gt;
  &lt;span class="c1"&gt;// The port usage bellow makes sure that the provider only gets created&lt;/span&gt;
  &lt;span class="c1"&gt;// when after the database is created and the tunnel is established&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jumphost_ssh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;my_database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local_port&lt;/span&gt; &lt;span class="c1"&gt;// This is the tunneled port&lt;/span&gt;
  &lt;span class="c1"&gt;// omitted the rest of the parameters&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or you can also use to reach services that you already have the host bust still need to proxy through the tunnel.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"jumphost_ssh"&lt;/span&gt; &lt;span class="s2"&gt;"my_api"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;hostname&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"httpbin.org"&lt;/span&gt;
  &lt;span class="nx"&gt;port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"http"&lt;/span&gt; &lt;span class="s2"&gt;"my_api"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:&lt;/span&gt;&lt;span class="k"&gt;${data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jumphost_ssh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;my_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;local_port&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/method"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"api_response"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;my_api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I hope this can help other people out there and don't forget to send issues and pull requests to my &lt;a href="https://github.com/jaysonsantos/terraform-provider-jumphost" rel="noopener noreferrer"&gt;github&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>tutorial</category>
      <category>ssh</category>
      <category>go</category>
    </item>
    <item>
      <title>Using terraform's remote-exec provider with AWS SSM</title>
      <dc:creator>Jayson Reis</dc:creator>
      <pubDate>Sat, 20 Feb 2021 07:48:40 +0000</pubDate>
      <link>https://dev.to/jaysonsantos/using-terraform-s-remote-exec-provider-with-aws-ssm-5po</link>
      <guid>https://dev.to/jaysonsantos/using-terraform-s-remote-exec-provider-with-aws-ssm-5po</guid>
      <description>&lt;p&gt;In an effort to harden our infrastructure at Grover, we decided to block all SSH connections, from both public and private subnets. Instead, we want to use AWS's SSM agent to manage SSH connections so we don't have to handle each of the machine's authorized keys. Users can connect with the provided tool as long as they have the right IAM permissions.&lt;/p&gt;

&lt;p&gt;This worked great! I mean, almost perfect. Well, some stuff broke. There were problems because some of our terraform modules used provisioners with remote-exec so we have fine-grained control on when the creation of the machine failed.&lt;/p&gt;

&lt;p&gt;To fix it, we tried to &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-getting-started-enable-ssh-connections.html" rel="noopener noreferrer"&gt;set up an ssh_config&lt;/a&gt; and change the host where it tried to connect, so terraform could execute the proxy command to connect and then do its thing. But it seemed that terraform was not reading the config at all so that just failed. I assumed that terraform was using some go module to connect and not something like libssh.&lt;/p&gt;

&lt;p&gt;Then I thought I could run ssh on a local-exec provider with a random port forwarding to the target machine, then going to the background so it would go to the next provisioner on our module. But instead of going to the next step, it would always get stuck, waiting for it to finish somehow no matter how I tried running it. I tried &lt;code&gt;ssh -f&lt;/code&gt;, &lt;code&gt;nohup ssh -f &amp;amp; &amp;amp;&amp;amp; disown&lt;/code&gt;, but I was getting nowhere and decided to take a look on the go code.&lt;/p&gt;

&lt;p&gt;First I saw &lt;a href="https://github.com/hashicorp/terraform/blob/4e345b6d27b28d22a6f465dabd5a182e9b8ee8b5/builtin/provisioners/local-exec/resource_provisioner.go#L117" rel="noopener noreferrer"&gt;that the code opens a pipe&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pipe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But a few &lt;a href="https://github.com/hashicorp/terraform/blob/4e345b6d27b28d22a6f465dabd5a182e9b8ee8b5/builtin/provisioners/local-exec/resource_provisioner.go#L154" rel="noopener noreferrer"&gt;lines later&lt;/a&gt; it just waits for the process to finish.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And I thought: "Well when I disown, the bash program should have finished without any children as disown would move the subprocess up into the tree". And it indeed was working properly.&lt;br&gt;
A few lines later, I noticed a &lt;a href="https://github.com/hashicorp/terraform/blob/4e345b6d27b28d22a6f465dabd5a182e9b8ee8b5/builtin/provisioners/local-exec/resource_provisioner.go#L164-L167" rel="noopener noreferrer"&gt;select on a couple of channels&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;copyDoneCh&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See that &lt;code&gt;copyDoneCh&lt;/code&gt;? It is used &lt;a href="https://github.com/hashicorp/terraform/blob/4e345b6d27b28d22a6f465dabd5a182e9b8ee8b5/builtin/provisioners/local-exec/resource_provisioner.go#L187-L193" rel="noopener noreferrer"&gt;on a function&lt;/a&gt; that keeps reading the pipe until it is finished and then it gets closed when the function is finished.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;copyUIOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="n"&gt;provisioners&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UIOutput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doneCh&lt;/span&gt; &lt;span class="k"&gt;chan&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="nb"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doneCh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;lr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;linereader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;lr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember that opened pipe on the top? Well, my naïve approach with &lt;code&gt;ssh -f&lt;/code&gt; was inheriting the main process' pipe and never closing it when ssh went to the background.&lt;br&gt;
So, it was a matter of making the process not inherit them so my bash script could and terraform could move to the next step.&lt;/p&gt;

&lt;p&gt;To test this, you can start with a &lt;a href="https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource" rel="noopener noreferrer"&gt;null_resource&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"null_resource"&lt;/span&gt; &lt;span class="s2"&gt;"test"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;provisioner&lt;/span&gt; &lt;span class="s2"&gt;"local-exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;command&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"ssh-port-forward.sh"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;interpreter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-c"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;INSTANCE_ID&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"i-bebacafe"&lt;/span&gt;
      &lt;span class="nx"&gt;USERNAME&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"your-user"&lt;/span&gt;
      &lt;span class="nx"&gt;RANDOM_PORT&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;random_integer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ssh_port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;provisioner&lt;/span&gt; &lt;span class="s2"&gt;"remote-exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;inline&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"echo hello world"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;connection&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt;
      &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;random_integer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ssh_port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;
      &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"your-user"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"random_integer"&lt;/span&gt; &lt;span class="s2"&gt;"ssh_port"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;min&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10000"&lt;/span&gt;
  &lt;span class="nx"&gt;max&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"60000"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then for the bash script that creates the port fowarding.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-ex&lt;/span&gt;
&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INSTANCE_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;echo &lt;/span&gt;missing INSTANCE_ID&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USERNAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;echo &lt;/span&gt;missing USERNAME&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RANDOM_PORT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;echo &lt;/span&gt;missing RANDOM_PORT&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; +e

cleanup&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;cat &lt;/span&gt;log.txt
    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; log.txt
    &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="nv"&gt;$!&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;try &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;0..25&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Trying to port forward retry #&lt;/span&gt;&lt;span class="nv"&gt;$try&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="c"&gt;# The following command MUST NOT print to the stdio otherwise it will just&lt;/span&gt;
    &lt;span class="c"&gt;# inherit the pipe from the parent process and will hold terraform's lock&lt;/span&gt;
    ssh &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;-oStrictHostKeyChecking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USERNAME&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="nv"&gt;$INSTANCE_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="s2"&gt;"127.0.0.1:&lt;/span&gt;&lt;span class="nv"&gt;$RANDOM_PORT&lt;/span&gt;&lt;span class="s2"&gt;:127.0.0.1:22"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nb"&gt;sleep &lt;/span&gt;1h &amp;amp;&amp;gt; log.txt  &lt;span class="c"&gt;# This is the special ingredient!&lt;/span&gt;
    &lt;span class="nv"&gt;success&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$success&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;cleanup 0
    &lt;span class="k"&gt;fi
    &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;5s
&lt;span class="k"&gt;done

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Failed to start a port forwarding session"&lt;/span&gt;
cleanup 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how on the ssh command I redirect all the output to a file with &lt;code&gt;&amp;amp;&amp;gt; log.txt&lt;/code&gt;? This is to avoid its process to inherit the pipes I was talking about before.&lt;br&gt;
After this, your plan should just work the way you expect it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;null_resource.test: Creating...
null_resource.test: Provisioning with 'local-exec'...
null_resource.test (local-exec): Executing: ["bash" "-c" "#!/usr/bin/env bash\nset -ex\ntest -n \"$INSTANCE_ID\" || (echo missing INSTANCE_ID; exit 1)\ntest -n \"$USERNAME\"    || (echo missing USERNAME; exit 1)\ntest -n \"$RANDOM_PORT\" || (echo missing RANDOM_PORT; exit 1)\n\nset +e\n\ncleanup() {\n    cat log.txt\n    rm -rf log.txt\n    exit $!\n}\n\nfor try in {0..25}; do\n    echo \"Trying to port forward retry #$try\"\n    # The following command MUST NOT print to the stdio otherwise it will just\n    # inherit the pipe from the parent process and will hold terraform's lock\n    ssh -f -oStrictHostKeyChecking=no \\\n        \"$USERNAME@$INSTANCE_ID\" \\\n        -L \"127.0.0.1:$RANDOM_PORT:127.0.0.1:22\" \\\n        sleep 1h &amp;amp;&amp;gt; log.txt\n    success=\"$?\"\n    if [ \"$success\" -eq 0 ]; then\n        cleanup 0\n    fi\n    sleep 5s\ndone\n\necho \"Failed to start a port forwarding session\"\ncleanup 1"]
null_resource.test (local-exec): + test -n i-bebacafe
null_resource.test (local-exec): + test -n your-user
null_resource.test (local-exec): + test -n 45160
null_resource.test (local-exec): + set +e
null_resource.test (local-exec): + for try in {0..25}
null_resource.test (local-exec): + echo 'Trying to port forward retry #0'
null_resource.test (local-exec): Trying to port forward retry #0
null_resource.test (local-exec): + ssh -f -oStrictHostKeyChecking=no your-user@i-bebacafe -L 127.0.0.1:45160:127.0.0.1:22 sleep 1h
null_resource.test (local-exec): + success=0
null_resource.test (local-exec): + '[' 0 -eq 0 ']'
null_resource.test (local-exec): + cleanup 0
null_resource.test (local-exec): + cat log.txt
null_resource.test (local-exec): + rm -rf log.txt
null_resource.test (local-exec): + exit
null_resource.test: Provisioning with 'remote-exec'...
null_resource.test (remote-exec): Connecting to remote host via SSH...
null_resource.test (remote-exec):   Host: 127.0.0.1
null_resource.test (remote-exec):   User: your-user
null_resource.test (remote-exec):   Password: false
null_resource.test (remote-exec):   Private key: false
null_resource.test (remote-exec):   Certificate: false
null_resource.test (remote-exec):   SSH Agent: true
null_resource.test (remote-exec):   Checking Host Key: false
null_resource.test (remote-exec): Connected!
null_resource.test (remote-exec): hello world
null_resource.test: Creation complete after 3s [id=1103075062717627803]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I hope this helps someone out there as it took me a bunch of hours to figure out what the problem was.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>aws</category>
    </item>
  </channel>
</rss>
