A small language for work that is not.

Built for jobs where a failure costs money. A mistake stops at the line that made it, or before the program starts, and every error is a value your code can catch.

curl -fsSL ecko.sh/install | sh

Errors are values

import std.json

rows = [r"""{"cents":1250}""", r"""{"cents":""", r"""{"cents":990}"""]

# A malformed row is skipped. Any other error stops the job.
fn cents(row) = try {
    json.decode(row).cents
} catch (e) {
    if get(e, "kind") == "parse" { null } else { error(e) }
}

good = rows
    |> map(cents)
    |> filter(fn(c) c != null)
print("{sum(good)} cents, {len(rows) - len(good)} skipped")

                

Try other examples in the playground

Why Ecko?

Four reasons, starting with the one we can measure.

No warm-up tax

Cold start · hello world to first byte out
  • ecko

    2.5ms

  • python

    9.9ms

  • node

    19.4ms

  • ruby

    46.3ms

Median of 100 runs; the 10 runs before them were discarded. Ecko v0.62.0, python 3.12.3, node 24.4.0, ruby 3.2.3, on an AMD Ryzen 9 5950X under Linux.

Against AOT-compiled languages the gap is smaller. But they ask for a build step and a linker before you’ve written a line. Ecko skips both.

A bytecode compiler and a stack VM, with no tier that has to get hot. The thousandth run costs what the first one did. CLIs, serverless handlers and dev reloads are up before your finger leaves the key.

  • No garbage-collection pauses

    Memory is freed when the last reference goes away. Nothing stops the process to sweep, so a request at the tail isn’t waiting on a collector pass.

    fn handle(body) {
        n = len(body)
        n
    }
    # body drops at return - no GC pause later
    

    reference counted · freed at scope exit

  • Concurrency without the ceremony

    Parallel work is something you write, not something you configure. Each worker gets its own snapshot, so a batch job can fan out across a dataset with no shared state to get wrong.

    amounts = [1250, 900, 3400]
    fees = pmap(amounts, fn(a) a * 3 / 100)
    

    channel() · async fn · await · pmap()

  • Checked before it runs

    Every program is checked before its first side effect, including one a model wrote. An undefined name or a wrong argument count stops the run instead of letting it half-finish. ai[T] validates the reply against the type you declared, and with no API key it answers from a deterministic offline mock.

    type Lead = Hot { company: String, seats: Int }
        | Demo
    
    lead = ai[Lead] "Acme wants 40 seats"
    

    ai[T] · checked against your types · mock mode offline

How a 1.3-billion-record backfill ran on Ecko →

The language server ships with it.

It is ecko lsp, inside the same binary you install. Diagnostics, completion, hover and go-to-definition in any editor that speaks LSP. First-party extensions for VS Code, Cursor and JetBrains; everything else points at the same server.

Any editor that speaks LSPcommand ecko · argument lsp · files *.ecko
// Zed - settings.json
{
  "lsp": {
    "ecko": { "binary": { "path": "ecko", "arguments": ["lsp"] } }
  },
  "languages": {
    "Ecko": { "language_servers": ["ecko"] }
  }
}

// JetBrains, without the plugin: LSP4IJ, command ecko, argument lsp

Keep ecko on your PATH - or give the absolute path. Highlighting works without it; diagnostics, completion and hover wait for the server. Full editor setup →

Install it in one command.

A mistake stops the same way in a billion-row batch job as in a single request. One binary, four platforms, nothing else to install. If it turns out not to be for you, it is one file to delete.

curl -fsSL ecko.sh/install | sh

Homebrew: brew install ecko-lang/tap/ecko · binaries and checksums →