Globals
The names a program has in scope with no import.
Printing and tests
print(…) | any number of values, separated by a space |
assert(condition), assert(condition, message) | raises where the condition is false |
assertEq(got, wanted) | renders both sides, quoting a string |
The two assertions raise rather than answer, because a failed assertion is not a condition the test was going to handle. See Tests.
Kinds and conversions
string number integer real boolean
The type words are the conversions, and the same word tests in a pattern.
An array, a string and a range each answer .length; an object counts its own keys through
keys(o).length instead, .length being a property of arrays, strings and
ranges and not of objects.
print(string(123) + "!")
print(number("42"), number("nonsense"))
print(integer(2.9))
print(boolean(0)) // slate's truthiness is JavaScript's: zero is false
print("日本語".length) // in characters
123!
42 null
2
false
3
Text
chars split join contains includes indexOf lastIndexOf startsWith endsWith
trim trimStart trimEnd upper lower toUpperCase toLowerCase normalize casefold
replace replaceAll repeat padStart padEnd at
Every position is in characters, never in bytes.
indexOfanswersnullrather than-1, because slate has a null and the operators that go with it:indexOf(s, x) ?? 0says what a sentinel makes a reader work out.- A search may be told where to start.
indexOf(s, x, from)andlastIndexOf(s, x, from)take a third position, and a negative one counts back from the end. spliton an empty separator splits into characters.- Case is the whole Unicode database, so
upper("Straße")isSTRASSEandlower("ΟΔΟΣ")ends in a final sigma. A case mapping may change a string’s length; both back ends answer the same thing. trimis Unicode’sWhite_Space, so a no-break space comes off and a zero-width no-break space — which the database does not call a space — stays.normalize(s, form)takes one of"NFC","NFD","NFKC"and"NFKD"and faults on anything else rather than falling to a default.casefoldis notlower. It is the key to store beside a name somebody will search for:ßfolds toss, folding is idempotent, and what it answers is composed.includesiscontainsandreplaceAllisreplace, under the names JavaScript gave them.replacehas always changed every occurrence, soreplaceAllis not a second behaviour — the JavaScript habit of reaching for the “all” name is simply right here.padStartandpadEndbring a string up to a width, in characters. The filler REPEATS and is cut to fit, sopadStart("7", 5, "ab")is"abab7"; a string already at or past the width is answered unchanged.toUpperCaseandtoLowerCaseareupperandlowerunder JavaScript’s spelling, and they are the same natives rather than a second implementation. Neither pair is deprecated: the short names read better in slate’s own prose, and these are what a reader arriving from JavaScript writes first.at(s, i)reads one character, counting back from the end where the position is negative — which is whatxs.at(-1)is for an array, and is why JavaScript grewatbesides[i]. It counts in characters like everything else, so"a👋".at(-1)is the wave and not half of it, and a position past either end is a fault rather than an absence.
print("abc".at(-1), "a👋".at(-1))
print("straße".toUpperCase(), "ΟΔΟΣ".toLowerCase())
c 👋
STRASSE οδος
print(upper("Straße"), lower("ΟΔΟΣ"))
print(casefold("STRASSE") == casefold("Straße"), lower("STRASSE") == lower("Straße"))
print(normalize("e\u{301}", "NFC") == "é")
print(padStart("7", 3), padEnd("7", 3, "0"))
print(replaceAll("a,b,c", ",", "-"), includes("hello", "ell"))
STRASSE οδος
true false
true
7 700
a-b-c true
length is a property, not a method — s.length counts characters, never UTF-16 units, so
"a👋".length is 2. Writing to it is refused.
As methods, a string answers: chars split contains includes indexOf lastIndexOf startsWith endsWith trim trimStart trimEnd upper lower toUpperCase toLowerCase normalize casefold replace replaceAll repeat padStart padEnd at number integer real boolean string.
A for walks a string’s characters, which is what every other name on a string counts in and is
what JavaScript’s for...of yields — so chars(s) is only wanted where the array itself is:
for c in "a👋b"
print(c)
a
👋
b
Numbers
abs floor ceil round trunc sqrt pow min max random isNaN isFinite
toFixed toExponential toPrecision formatNumber
sin cos tan asin acos atan atan2 sinh cosh tanh log log2 log10 exp cbrt hypot
PI E
- The four roundings leave an integer alone, an integer already being whole.
minandmaxtake as many arguments as they are given and answer an integer when every one of them was — aminthat answered a real for two integers would make every use of it in an index a conversion.powof two integers with a non-negative exponent answers an integer.isNaN(v)andisFinite(v)areNumber.isNaNandNumber.isFinite, not JavaScript’s bare globals. Nothing is converted, so a value that is not a number is simply not NaN and not finite either —isNaN("x")isfalsehere where JavaScript’s global saystrue. A program asking about text writesisNaN(number(s)), which says what it is doing. A whole number is finite and is never NaN, however big it has grown.isNaNis the one way to ask, since a program may not rest onx == xanswering false: slate’s==issame, which a class may decide for itself. Both are calls rather than methods, a method being found by the kind of its receiver.random()answers a real in[0, 1)and takes no arguments, which is JavaScript’sMath.random()under its own name — 53 bits, and never1.0. It is not a source of secrets: a handful of draws gives away every draw that follows, so a key, a token, a nonce or a password reset wantsrandomBytesinslate:cryptoinstead.toFixed(x, places)writes a number to exactly that many decimal places, as text. It is byte-identical with JavaScript’s, ties included: a tie rounds AWAY from zero, which is not what C’sprintfdoes. It never faults on the number itself — past1e21it gives up on decimal places and answers whatstringwould in exponential form,NaNanswers"NaN", and an infinity answers"Infinity"/"-Infinity". Only the place count, outside0..100, is refused.toExponential(x)andtoExponential(x, digits)write a number in scientific notation. With no count it uses as many digits as the value needs to name itself — the same digitsstringwould show — sotoExponential(0.1)is1e-1. With one it writes exactly that many after the point, correctly rounded, and the count runs from 0 to 100. A negative zero loses its sign here, which is JavaScript’s answer too.toPrecision(x)andtoPrecision(x, digits)count SIGNIFICANT digits rather than decimal places, and the count runs from 1 to 100. With none it isstring(x). The layout is chosen by the exponent and not by the magnitude: plain while the exponent sits in-6 <= e < digitsand exponential outside it, sotoPrecision(123456, 2)is1.2e+5wherestring(123456)is123456.- All three round a tie AWAY from zero on the exact value the number holds, which is
JavaScript’s rule and not C’s
printf‘s, and neither of these faults on the number either —NaN, an infinity and a whole number past2^53all answer rather than refuse. Only the digit count, outside its range, is refused — and these two do not even read it until the value is known to be finite, sotoExponential(x, 500)of aNaNis"NaN"wheretoFixed‘s own range refusal would fire. Both orders are JavaScript’s, read literally. formatNumbergroups the digits in threes, taking a whole number or the texttoFixedanswered — never areal, which has no one written form of its own.formatNumber(toFixed(total, 2))is a price. The defaults are a comma and a full stop;{ separator: " ", decimal: "," }is the French convention, said with the same two strings rather than a locale name.- The trigonometric, hyperbolic and logarithmic functions are bare names, the way
sqrtis — there is noMathobject to reach through.sin cos tantake radians andasin acos atananswer them;atan2(y, x)keepsMath.atan2‘s own argument order,ybeforex.logis the natural logarithm,log2andlog10the other two bases, andexpislog‘s inverse.cbrtis the real cube root, defined for a negative number wheresqrtrefuses one.hypot(a, b)is the length of a right triangle’s hypotenuse. A domain error answersNaNor-Infinity, not a fault —log(0)andlog(-1)are exactly what JavaScript’sMath.loganswers, on both back ends. PIandEare the two constants, not calls —PIrather thanPI().- Where a result is not exactly representable the last bit is the machine’s, so
cbrt(27)andlog(E)are compared within a tolerance rather than printed: one C library answers them exactly and another a bit out, andprintnow shows every digit it is holding.
print(sin(PI / 2), cos(0), atan2(1, 1) == PI / 4)
print(log10(1000), exp(0), hypot(3, 4), log2(8))
print(log(0), log(-1))
1 1 true
3 1 5 3
-Infinity NaN
print(isNaN(0.0 / 0.0), isNaN(1.5), isNaN("x"))
print(isFinite(1), isFinite(1.5), isFinite(1.0 / 0.0), isFinite("x"))
true false false
true true false false
print(toFixed(1234.5, 2), toFixed(2.5, 0))
print(formatNumber(1234567), formatNumber(toFixed(1234.5, 2)))
print(formatNumber(toFixed(1234.5, 2), { separator: " ", decimal: "," }))
print(toFixed(1e21, 2), toFixed(1.0 / 0.0, 2), toFixed(0.0 / 0.0, 2))
1234.50 3
1,234,567 1,234.50
1 234,50
1e+21 Infinity NaN
print(toExponential(0.1), toExponential(123456), toExponential(1.5e21))
print(toExponential(255, 2), toExponential(9.99, 1))
print(toPrecision(123.456, 4), toPrecision(0.000123, 2), toPrecision(123456, 2))
print(toPrecision(1e21, 3), toPrecision(1.5))
1e-1 1.23456e+5 1.5e+21
2.55e+2 1.0e+1
123.5 0.00012 1.2e+5
1.00e+21 1.5
A draw cannot be printed and checked — what it is can:
val r = random()
print(r >= 0 && r < 1, r is real)
true true
As methods, a number answers: abs floor ceil round trunc sqrt sin cos tan asin acos atan sinh cosh tanh log log2 log10 exp cbrt integer real boolean string toFixed toExponential toPrecision formatNumber. atan2, hypot,
pow, min and max take a second number and stay bare calls, the way pow already does.
Arrays
push pop shift unshift insert removeAt clear
map filter flatMap forEach reduce find findIndex findLast findLastIndex every some
sort sorted reverse reversed toReversed slice at concat flat sum join contains
indexOf lastIndexOf keys values entries zip array
print([4, 1, 3, 2].sorted().slice(1, 3).flatMap(n -> [n, n]).at(-1))
print([1, 2, 3].sum(), [1, [2, [3]]].flat())
print(indexOf([1, 2], 9), find([1, 2, 3], n -> n > 1))
print([1, 2, 3].at(-1), [1, 2, 3].reversed())
3
6 [1, 2, [3]]
null 2
3 [3, 2, 1]
An array answers the same three walks an object answers, and its key is the POSITION.
xs.entries() is [[0, x0], [1, x1], …], which is what makes for [i, x] in xs.entries() the
indexed walk — the one loop slate had no spelling for, for i, x in xs being refused by the syntax
rule and a map already walking as pairs. keys answers the positions and values a copy of the
elements, which is JavaScript’s Array.prototype.keys/values/entries with an array in place of
the iterator.
toReversed is reversed under JavaScript’s spelling and is the same native: the bare verb
changes the array and the participle answers a new one, which is the pair JavaScript reached too.
val xs = ["a", "b", "c"]
print(xs.keys(), xs.values(), xs.entries())
for [i, x] in xs.entries()
print(i, x)
print([1, 2, 3].toReversed())
[0, 1, 2] ["a", "b", "c"] [[0, "a"], [1, "b"], [2, "c"]]
0 a
1 b
2 c
[3, 2, 1]
array(x) answers everything x yields, in an array — the type word is the conversion, which is
the rule string, integer, real and boolean already follow. What it takes is exactly what a
for walks and what ... spreads: an array, bytes, a range, a generator, a set or a map, a map
yielding its pairs. An array given to it is copied, which is what a conversion means everywhere
else. A string is deliberately not on that list — chars(s) is how slate spells one as its
characters.
zip walks every side rather than indexing it, so the same list goes in there: zip(0..<3, names)
is the numbered walk a program writes it for, and the shortest side decides the length.
squares()
yield 1
yield 4
print(array(0..<4), array(Set([1, 2, 2])), array(toBytes("hi")))
print(array(squares()), [...squares()])
print(zip(0..<3, ["a", "b", "c"]))
[0, 1, 2, 3] [1, 2] [104, 105]
[1, 4] [1, 4]
[[0, "a"], [1, "b"], [2, "c"]]
length is a property and not a method, so it is read with no brackets after it — xs.length is
xs.length, and writing to it is refused rather than resizing the array:
print([1, 2, 3].length, [].length)
print(map(["a", "bb"], _.length))
3 0
[1, 2]
A builtin is counted where the call is written. Its parameters are fixed, so the checker can see every call made by name and refuses one giving too few or too many, in the same words it uses for a function you wrote — see the call rule. Reached through a value instead, a builtin obeys the run-time half like anything else: the surplus is dropped, and a builtin that did not get the value it needs says what it wanted.
A CALLBACK IS HANDED THE ELEMENT, ITS POSITION AND THE ARRAY, which is what JavaScript hands
one. reduce puts the running answer in front of those three. A function declares as many of them as
it wants and is given that many — every call drops what it cannot bind — so x -> x * 2 is written
exactly as it always was, and the index is there when you want it with no loop beside the call:
print(map(["a", "b", "c"], (letter, i) -> s"${i}:${letter}"))
print(filter([10, 20, 30, 40], (n, i) -> i % 2 == 0))
print(reduce([5, 6, 7], (total, n, i, all) -> total + n * i + all.length, 0))
["0:a", "1:b", "2:c"]
[10, 30]
29
The third argument is the array being walked, so a callback can look at what the walk has not reached yet. Changing that array while it is being walked is a fault rather than a short answer:
val xs = [1, 2, 3]
print(map(xs, (n, i, all) -> if i + 1 < all.length then all[i + 1] else 0))
[2, 3, 0]
A callback declaring a fourth parameter is refused, there being nothing to fill it with —
reduce‘s limit is five for the same reason:
map([1, 2], (n, i, all, spare) -> n)
`map` takes (integer, integer, array of integer) -> any here
sort and sorted are the exception, and not really one: their function is asked about a pair,
so there is no position to hand it.
Where slate parts from JavaScript it is to remove a case rather than add one:
- A mutator answers nothing, so
sortandunshiftcannot be mistaken for the copying formssortedandconcat. The bare verb changes the array and the participle answers a new one. - Nothing answers absence.
pop,shiftandatfault where there is nothing there, whilefindandindexOfanswernull— a search that found nothing is an answer, and reaching past the end is a mistake. atandslicecount back from the end where the position is negative, which is the whole reason JavaScript grewatbesidexs[i].- A comparator answers a boolean: whether the first value comes before the second.
sorted(xs, _.age < _.age)is the whole of one. A comparator answering-1,0or1is refused by name:-1and1are both true and0is false, so a three-way answer would read as “before, not before, before” rather than the ternary comparison it meant.
Objects
keys values entries has without freeze isFrozen
keys, values and entries answer for an ARRAY too, with the position for a key — see
Arrays above. Everything else here is an object’s alone, freeze and isFrozen excepted:
they answer for every kind.
val o = { a: 1, b: 2 }
print(keys(o), values(o), has(o, "a"))
print(without(o, "a"), o)
for [k, v] in entries(o)
print(k, v)
["a", "b"] [1, 2] true
{b: 2} {a: 1, b: 2}
a 1
b 2
without answers a NEW object, which is with‘s rule read the other way: a slate object’s shape
is not a thing one name changes under another’s feet. A key that is not there is not an error, so a
table forgetting something it is unsure of is one line — and a copy of a data value
is a data value, as with‘s is.
A proto a declaration wrote is not a field these report. A class instance and a data variant
reach what they are through it, so keys, values, entries, has and without all pass over it —
which is what print has always done. A proto a program wrote on a plain object is an ordinary
field and is reported like any other.
An object answers no methods of its own — its names belong to the program, and a builtin o.keys
would give every object a field nothing put there. The four universal methods are the
exception, and a field the program wrote wins over those.
freeze(v) closes a container to writing and answers the value it was given; isFrozen(v) says
whether writing through it is refused. Both take an object, an array, a set, a map, a buffer or a weak
map — and anything else answers itself and is already frozen, there being nothing to write through
a number or a string.
val limits = freeze({ retries: 3 })
val names = freeze(["a", "b"])
print(isFrozen(limits), isFrozen(names), isFrozen(7))
print(limits.retries, names[0], sorted(names))
print(isFrozen({ a: 1 }), freeze("hi"))
true true true
3 a ["a", "b"]
false hi
It is shallow, freezing twice is a no-op, and with, a spread, Set(s) and Map(m) all answer a
copy that is free to change. Objects is the page that says what a refused
write reads like.
Sets and maps
Set Map
The two constructors are the only globals here — everything else a set or a map does is a method or a property on the value itself, which Collections is the page for.
val seen = Set(["b", "a", "b"])
val ages = Map([["ada", 36]])
print(seen, seen.size, seen.has("a"))
print(ages, ages.get("ada"))
["b", "a"] 2 true
[["ada", 36]] 36
Either takes any value at all as a key, compared the way == compares it — which is what an
object, whose names are strings, cannot do.
JSON
parseJSON(text) | a result |
toJSON(v), toJSON(v, indent) | a string; faults where the value has no JSON form |
The two directions use the two channels, and they genuinely differ: text from a file, a socket or a
person is a condition every caller was going to handle, and a function or a circle in a value the program
built itself is a defect in that program. JSON.parse and JSON.stringify both throw, so node treats the
two alike.
- A value with no JSON form is named rather than dropped.
JSON.stringifydrops a function silently from an object and turns it intonullinside an array, so a request body goes out with a field missing and nothing says which. - A non-string key is refused, rendering it as text would make
{ 1: "a" }and{ "1": "a" }one document. - A class says what it encodes as with
toJSON(self)— see Objects. - The parse error is the whole rendering, not one sentence. A JSON document is usually machine-written and long, so “expected a string” on its own says nothing a person can act on.
Bytes
bytes(n) bytes(xs) bytes(b) | a buffer of that many zero bytes, of those numbers, or a copy |
toBytes(s) toBytes(s, encoding) | a buffer — UTF-8, or "latin1" |
fromBytes(b) fromBytes(b, encoding) | a result — arbitrary bytes are not text |
toArray(b) | the bytes as an array of numbers |
bytes is a kind of its own — one byte per byte, grown and cut with memory operations rather
than a loop. Bytes is the page: what a buffer can do, what the two encodings mean, and
what changed for a program that held bytes as an array of numbers.
toBytes(s).length is the byte count, so there is no third name. Two things need it: a
Content-Length, and a read where a character may be split across two arrivals.
print(toBytes("héllo").length, toBytes("héllo"))
6 <bytes 6: 68 c3 a9 6c 6c 6f>
Timers
setTimeout(fn, ms) setInterval(fn, ms) clearTimeout(id) clearInterval(id)
The callback comes first, which is node’s order. Both ms forms also take a
duration. A timer keeps the program alive.
Promises
sleep resolve reject pending settle fail
all allSettled race any
See Asynchrony.
The four that wait on many promises at once, which await cannot be written to do by itself: it
starts the second one only once the first has come back.
allanswers every value in the order given, a plain value in the array being one that has already arrived — a cache hit sits beside afetchwithout being dressed up as a promise. It fails with the first rejection in that same order, andall([])answers[].allSettledanswers the RESULT shape slate already has —{ ok: true, value }or{ ok: false, error }, the same shapeparseJSON, everySynccall inslate:fsandrunalready answer — and not JavaScript’s{ status, value | reason }.raceanswers whichever one settles first, success or failure.anyanswers the first to succeed, past any number of failures, and fails only where every one did — with every reason, since a slate rejection carries text and there is nothing for anAggregateErrorto be.race([])andany([])are refused rather than answered with a promise that never settles, which is what JavaScript does and is a program that hangs.
async first(ms, label)
await sleep(ms)
label
async main()
print(await all([1, resolve(2), 3]))
print(await allSettled([resolve(1), reject("bad")]))
print(await race([first(20, "slow"), first(5, "fast")]))
print(await any([reject("no"), resolve("yes")]))
main()
[1, 2, 3]
[{ok: true, value: 1}, {ok: false, error: "bad"}]
fast
yes
Generators
next(g), which is g.next() — see Asynchrony.
host
print(host())
interpreter
Which of the three hosts this program is running on, constant for the life of a program. These
pages run on the interpreter, so that is what prints above; the same call answers "node" under
slate js run with node, and "browser" in a browser. It takes no arguments and refuses one that
takes any.
A shared file is the reason this exists. A component with no control over where it runs cannot otherwise tell a browser-only call from one that will fault everywhere else — see the DOM for the case that asked for it.
fetch
val r = await fetch(url, options)
node’s name and the browser’s, and it answers a result rather than throwing — which is slate’s rule
for anything that reaches the network, not a disagreement with either. It is native because HTTPS needs
OpenSSL; the server half is slate:http and is written in slate.
It works on both back ends, over the host’s own fetch under slate js — a status, a headers
object with lower-cased names, and a body. Nothing in this file says “not in the JavaScript back end
yet” any more.
Two things differ there and JavaScript says why: trust refuses,
no JavaScript host letting a program add a certificate authority for one request, and the redirect
rule is the host’s — the interpreter follows at most five and refuses one that leaves https for
http, where a browser follows them itself and gives a page no way in.
An https fetch speaks HTTP/2 where the server offers it, the client offering h2 and
http/1.1 in the TLS handshake and the server choosing; a server that chooses http/1.1, or names
nothing, is spoken to in HTTP/1.1 exactly as before. Plain http is always HTTP/1.1, there being no
HTTP/2 in the clear, which is what browsers do too. Nothing about the answer says which was
spoken: the status, the headers, timeout, stream, the decoding and the redirects are the same
on both. A stream the server resets, a GOAWAY that leaves the request unanswered and a frame that
breaks the protocol each answer { ok: false, error } with a sentence that says which —
“the server reset the request for https://example.com/ (INTERNAL_ERROR)”. Under slate js the
version is the host’s choice. A build without the http2 feature speaks HTTP/1.1 alone.
A response header that repeats is joined with ", ", on both. An object has one value per name
and HTTP does not: Link, Vary and Via all repeat, and combining them is what RFC 9110 allows
and what a browser’s Headers hands over already done.
Set-Cookie is the exception and is a LIST, of the lines as they arrived, absent when there were
none. The same section of RFC 9110 excludes it and the reason is arithmetic: a cookie carries commas
inside itself — Expires=Wed, 21 Oct 2026 07:28:00 GMT — so two cookies joined by commas cannot be
taken apart again by anything. In a browser it is absent however many arrived: Set-Cookie is a
forbidden response-header name, so a page never reads one. The browser still applies the cookie.
A body that is not UTF-8 is "" rather than a string of replacement characters, which is the same
answer run gives and for the same reason: slate has no byte value for it to be.
The options are method, body, headers, trust, timeout, stream and reuse, all
optional.
Connections are pooled and reused, per origin. Over HTTP/1.1 a connection whose answer was read
to its last byte — a Content-Length met, a chunked body read through its trailers, or a reply with
no body — goes back to the pool unless either side said Connection: close, and the next request to
the same scheme, host and port takes it instead of dialling. An origin keeps at most six idle
connections, and one left idle for about four seconds is closed. An idle connection never keeps a
program running: nothing reads it and its timer is in the background, so a program that fetched
once ends when its own work does. A request that finds its pooled connection closed by the server —
refused on the write, or ended before a byte of the answer — is sent again once on a fresh one when
its method is idempotent (GET, HEAD, OPTIONS, TRACE, PUT, DELETE); a POST is told the
connection went, since the server may have acted on it.
Over HTTP/2 an origin has ONE connection and every concurrent fetch to it is a stream on it.
Fetches made while the first dial is still finding out whether the server speaks h2 wait for that
answer rather than each dialling their own. Streams past the server’s MAX_CONCURRENT_STREAMS wait
their turn on the same connection. After a GOAWAY the streams the server said it will answer finish
there, and the next fetch dials a new connection. A streamed body nobody is reading holds back only
its own stream: the server stops sending it at its window while every other stream keeps flowing.
reuse: false opts one request out: it dials a connection of its own, asks for
Connection: close over HTTP/1.1, and closes it when it is done — for a request that must not ride on
state another left behind. It is not called keepalive because a browser’s fetch already has that
name for something else: a request allowed to outlive the page that made it. Under slate js it is
read and then left to the host, whose pool is its own and which offers a request no way out of it;
anything but true or false is refused on both. Host carries the port wherever it is not the
scheme’s own, as RFC 9110 says.
stream: true hands the body over as it arrives. The answer is the same { ok, value }, settled
once the head is in, and value.body is an asynchronous source of bytes — the same kind
slate:gzip takes — read with for await:
val r = await fetch(url, { stream: true })
if r.ok
using body = r.value.body
for await chunk in body
chunk match
{ ok: false, error } -> print("broke off: " + error)
_ -> save(chunk)
- A chunk is whatever has arrived since the last one was asked for, whether the server framed the
body with
Content-Length, with chunks, or by closing the connection. Nothing is read ahead of the program by more than one read, so a slow reader costs the server’s send buffer and not this process’s memory. - A body that breaks off is an answer, not a fault. A connection dropped in the middle of the body,
a limit running out, or a damaged chunk hands over what had arrived and then one last value,
{ ok: false, error }— “http://example.com/bigstopped sending in the middle of the body” — after which the source is done. The bytes came from outside the program, so this is the result channel and afor awaitsees an ordinary value. timeoutstill covers the whole exchange, the body included: a limit that runs out while the body is flowing ends it with the same sentence a fetch that never answered gets.- Stopping early closes the connection. A body has a
return, so afor awaitover it that is left before the end — abreak, areturn, a fault — closes it, as a loop left early closes whatever it walked. A body is also a resource with adispose, sousing body = …closes it on every way out of a block, which is what covers a body that is never looped over at all. A body read to its end has already closed it. One left unread, never looped over and not disposed holds its connection open, as a socket does.closeis the same call under the nameslate:httpasks a streamed response’s source for, so a fetched body can be handed straight back out as one. - A compressed body is decoded as it arrives, on both back ends: a streamed request asks for
gzip, deflateas a buffered one does, and each chunk is the bytes it decoded to. A stream damaged in the middle hands over what decoded before the damage and then ends in “the server said the body was compressed with gzip, and it could not be decompressed”. One stream that stops short of its end while its connection ends cleanly — the gzip trailer missing, say — is where the hosts part: the interpreter ends it in that same sentence, and node’sfetchanswers what it decoded as though it were whole. - Leaving
streamout, orstream: false, is the buffered fetch above, unchanged. Anything buttrueorfalseis a mistake in the program and faults at the call.
timeout limits the whole fetch — redirects and the body included — to a duration or a number of
milliseconds, exactly as run‘s does; 0 or leaving it out means no limit. When it runs out the
request is abandoned, its connection closed, and the answer is a result like any other unreachable
server: “http://example.com/ did not answer within 100 milliseconds”. A fetch that answers first
cancels its limit, so a generous one never holds a program open. The sentence is the same on both back
ends. A negative limit is a mistake in the program, so it faults at the call:
print(fetch("http://127.0.0.1:1/", { timeout: -5 }))
`timeout` cannot be negative, and this is -5 milliseconds
A compressed body arrives decoded. The interpreter asks for Accept-Encoding: gzip, deflate —
what node’s own fetch sends — and decodes a Content-Encoding: gzip or deflate body with the same
reader as slate:gzip, taking deflate as zlib or as the bare stream, as a browser does.
content-encoding stays in the headers, as it does in a JavaScript host’s: it is what the server said.
A body that will not decompress is an answer and not a fault — “the server said the body was
compressed with gzip, and it could not be decompressed”, on both back ends.
A caller who sets Accept-Encoding themselves has taken that over, and in the interpreter gets
the body exactly as the server sent it — which, compressed bytes not being text, is "". That is the
one difference between the hosts here: a JavaScript host decodes whatever arrives and gives a program
no way to stop it, and a browser does not let a page set Accept-Encoding at all.
JavaScript says so too.
In a browser a relative URL is resolved against the page’s own address, so fetch("/signup"),
fetch("./next") and fetch("../up") all reach the origin the page was served from — the resolving is
new URL(url, location.href), which is what the platform’s own fetch does with one. Under node and
the interpreter there is no page, so the same call is refused and says so: “/signup is not an http
or https URL, and there is no page to resolve it against”. A URL naming a scheme slate does not speak
keeps the plain sentence, a base being no help to it.