What a browser cannot give you — and why that is an error, not a crash

For app programmers August 2026 · from the RingScript project

Ring in a page is the same Ring: the same compiler, the same virtual machine, byte-identical output on roughly 850 verified programs. What changes is the world around it. This is the practical map of what your program loses, exactly how each loss behaves, and the one design rule that makes the difference between a caught error and a dead page.

The rule, first

Exclusion is an error, never a crash.

A Ring program that asks for something a browser does not have gets a catchable Ring error with a line number, and the VM — with every global, every class, every loaded library still in it — keeps running. Nothing in the verification corpus has ever brought the runtime down. That is the promise you can build on, and it is why the rest of this page is a table rather than a warning.

The map

What your code asks forWhat happens in a page
write, remove, rename, tempfile fails the way an unwritable file fails — a trappable Ring error (the R35 family)
read, load resolve against the embedded library map baked into the wasm; an unknown path is a trappable error
fexists, getfilesize, getpathtype answer from that same map, so if fexists(f) … read(f) behaves as written
direxists, directory listing always false, always empty — truthfully, because there are no directories to have
system(), chdir, currentdir compiled out entirely
threads, sockets, RingQt and other bindings not present — they are separate C extensions, never part of the core VM
syssleep returns immediately; a page does not get to block
clock(), time(), date() fully working, on the host's local clock
random() works — and is unseeded, exactly as in native Ring
iswindows(), filename(), pointer addresses answer truthfully for the wasm environment: 0, Ring_EmbeddedCode, and so on

Three of those deserve a paragraph

fexists was a real gap, and a good lesson

Redirecting fopen to the embedded map is the obvious half of having no filesystem. The half that is easy to miss: several Ring functions ask about a file instead of opening one. fexists, getfilesize and getpathtype go through stat, not fopen — so for a while they reported that embedded files did not exist, and the most idiomatic guard in Ring skipped files that were sitting right there:

if fexists("ringlib/stzZql.ring")     # said 0 — but the file is embedded
    load "ringlib/stzZql.ring"
ok

The fix was to redirect stat to the same resolver, so the question and the answer come from one place. The lesson generalises: when you replace a capability, find every door into it, not just the one your first test used.

random() repeats — for every visitor

Ring does not seed its generator, natively or here. On a desktop program run once a day that is a curiosity. On a page, it means every visitor gets the same sequence of numbers, in the same order, forever. If the randomness matters — sample data, a shuffled quiz, an id — seed it yourself:

nSeed = (clock() % 100000) + 1
for i = 1 to nSeed  x = random(100)  next    # burn a variable prefix

Or, better in a page: ask JavaScript, which has a real source of entropy, and pass the number in through ring.call.

Time works, and the timezone is done by hand

wasi-libc carries no timezone database, so a naive build reports UTC and your clock is politely wrong for most of the planet. RingScript applies the browser's own offset before handing the time to Ring, which is why date() and time() agree with the clock in the corner of the screen. It is a small thing that would have been a long-running bug report.

Shipping your own library into the page

Because load resolves against an embedded map, a pure-Ring library can travel into the browser unchanged. That is three steps:

# 1. drop mylib.ring into src/ringlib/
# 2. add one line to embedded_files in src/bridge.zig:
.{ .name = "ringlib/mylib.ring", .data = @embedFile("ringlib/mylib.ring") },
# 3. zig build serve

load "ringlib/mylib.ring"      # now works in any page

One contract keeps this honest: embedded libraries stay pure Ring — no file or OS calls. That is exactly what makes them portable into a browser without edits, and it is why the ZQL engine, written years earlier for a filesystem, needed none. If you are writing a Ring library today and would like it to run in a page tomorrow, that is the whole of the discipline.

Writing code that runs in both places

The practical shape is a seam, not a fork. Keep the rules, the calculations and the data structures in plain Ring that touches nothing but values — that part is identical on the server and in the page. Put the things that differ, storage above all, behind two or three functions, and give each environment its own implementation.

A page's version of "save" is a call out to JavaScript; a server's is a file. The 300 lines in between never learn which one they are talking to, and that is the entire portability story.

The full compatibility statement, with the corpus behind it: docs/compatibility.md. What the API looks like from both sides: docs/api.md.