Module net/tcp
Async TCP networking — typed TcpListener and TcpStream objects over
std/sys/tcp, with Exception-based error handling.
Every operation that touches the network is an io.async future, so it
runs on the ONE event-loop thread: awaiting a read parks this task and
lets every other task on the loop run, and nothing here blocks a thread.
Kernel failures are thrown as IoExn carrying an IoError (D1) instead
of being returned, which is why a resolved byte count is always a real
count and never a negative errno.
{ TcpListener, TcpStream } :: import("std/net/tcp");
{ SocketAddr } :: import("std/net/addr");
{ Exception, IoExn } :: import("std/error");
main :: (fn(io : Io, exn : Exception) -> unit)({
e := IoExn(io : io, exn : exn);
listener := io.await(TcpListener.bind(SocketAddr.loopback(u16(8080)), io), e);
stream := io.await(listener.accept(io), e);
io.await(stream.write_str("pong", io), e);
});
Stability
unstable — two shapes are still open, both in the READ direction. A
TcpStream can be written from safe code (write_str, write_string,
write_bytes) but can only be read through read(buf : *(u8), size, io),
so a program without pragma(Pragma.AllowUnsafe) has to go through the
Reader defaults or std/io/bufio's BufReader to get a byte out — the
same asymmetry std/net/udp closed on 2026-09-09 with recv_bytes, and
not yet closed here. And there is no incoming: Rust's listener iterator
needs the async iterator protocol Yo does not have (std/io/index.yo
names the same gap), so accept in a loop is the only spelling.
Freezing waits on a pointer-free read; the listener/stream names, which
mirror Rust's, are not expected to move.
Types
Which half of a TCP connection to shut down — the argument to
TcpStream.shutdown.
Variants
| Variant | Fields | Description |
|---|---|---|
Read | Further receives are disallowed (SHUT_RD). | |
Write | Further sends are disallowed (SHUT_WR). | |
Both | Both further sends and receives are disallowed (SHUT_RDWR). |
The stream of connections arriving at a TcpListener — Rust's
TcpListener::incoming, created by listener.incoming().
Item is a Result, not a bare TcpStream, for the reason Rust's is: a
single failed accept (a client that vanished between the SYN and the
accept, a per-process descriptor limit) is ONE bad connection, not the end
of the listener. Carrying the failure in the item is what lets the stream
survive it — and it is why Stream.next needs no Exception handler
(std/async/stream.yo).
The error is a NetError, the same type accept THROWS, so moving a
server loop from accept to incoming does not change its error
vocabulary.
The stream finishes (.None) once the listener is closed — including by
another task while a next is pending — mirroring Watcher.
Fields
| Name | Type | Description |
|---|---|---|
_listener | TcpListener |
Trait Implementations
impl(generic(S : Type), where(S <: Stream), S : (Stream))
map : fn(generic(A, B, F) self : S : (Stream), f : F : (Fn(A) -> B)) -> StreamMap(S : (Stream), B, F : (Fn(A) -> B))filter : fn(generic(A, F) self : S : (Stream), f : F : (Fn(A) -> bool)) -> StreamFilter(S : (Stream), F : (Fn(A) -> bool))Parameters
| Name | Type | Notes |
|---|---|---|
self | S : (Stream) | |
f | F : (Fn(A) -> bool) |
Returns: StreamFilter(S : (Stream), F : (Fn(A) -> bool))
filter_map : fn(generic(A, B, F) self : S : (Stream), f : F : (Fn(A) -> Option(B))) -> StreamFilterMap(S : (Stream), B, F : (Fn(A) -> Option(B)))Parameters
| Name | Type | Notes |
|---|---|---|
self | S : (Stream) | |
f | F : (Fn(A) -> Option(B)) |
Returns: StreamFilterMap(S : (Stream), B, F : (Fn(A) -> Option(B)))
take : fn(generic(A) self : S : (Stream), n : usize) -> StreamTake(S : (Stream))skip : fn(generic(A) self : S : (Stream), n : usize) -> StreamSkip(S : (Stream))for_each : fn(generic(A, F) self : S : (Stream), f : F : (Fn(A) -> unit), io : Io) -> Impl : (Future[Future](unit) Io : Io)impl(Incoming, Stream(...))
A TCP socket that listens for incoming connections.
Fields
| Name | Type | Description |
|---|---|---|
_fd | i32 | |
_local_addr | SocketAddr | |
_is_closed | bool |
Trait Implementations
impl(TcpListener, ...)
bind : (TcpListener) fn(addr : SocketAddr, io : Io) -> Impl : (Future[Future](TcpListener) IoExn : IoExn)Bind to a socket address and start listening.
Parameters
| Name | Type | Notes |
|---|---|---|
addr | SocketAddr | |
io | Io |
Returns: Impl : (Future[Future](TcpListener) IoExn : IoExn)
accept : (TcpListener) fn(self : TcpListener, io : Io) -> Impl : (Future[Future](TcpStream) IoExn : IoExn)Accept an incoming connection, returning a new TcpStream.
Parameters
| Name | Type | Notes |
|---|---|---|
self | TcpListener | |
io | Io |
incoming : (TcpListener) fn(self : TcpListener) -> IncomingThe connections arriving at this listener, as a Stream of
Result(TcpStream, NetError) — Rust's TcpListener::incoming.
conns := listener.incoming().take(usize(3));
io.await(conns.for_each(c => { handle(c); }, io), io);
Every std/async/stream combinator applies, and a function can be
generic over where(S <: Stream) instead of over this listener.
Parameters
| Name | Type | Notes |
|---|---|---|
self | TcpListener |
Returns: Incoming
local_addr : (TcpListener) fn(self : TcpListener) -> SocketAddrGet this end's socket address — Rust's TcpStream::local_addr. Read
from the kernel once, when the stream was connected or accepted, because
that is the only way to learn the ephemeral port and the source
interface the kernel chose; neither is in the connect argument.
Parameters
| Name | Type | Notes |
|---|---|---|
self | TcpListener |
Returns: SocketAddr
close : (TcpListener) fn(self : TcpListener, io : Io) -> Impl : (Future[Future](unit) IoExn : IoExn)fd : (TcpListener) fn(self : TcpListener) -> i32impl(TcpListener, Dispose(...))
dispose : (TcpListener) fn(self : TcpListener) -> unitRelease the resources self owns — a file descriptor, a socket, a lock,
a buffer the allocator handed out. Called automatically when the last
reference to the value goes away, so an implementor never calls it
directly and must tolerate being the only one who ever does.
It must be safe to run exactly once: the runtime calls it at refcount
zero, and a type that also exposes an explicit close/release is
responsible for making the second call a no-op.
Parameters
| Name | Type | Notes |
|---|---|---|
self | TcpListener |
Returns: unit
A connected TCP stream for bidirectional data transfer.
Fields
| Name | Type | Description |
|---|---|---|
_fd | i32 | |
_peer_addr | SocketAddr | |
_local_addr | SocketAddr | |
_is_closed | bool |
Trait Implementations
impl(TcpStream, ...)
connect : (TcpStream) fn(addr : SocketAddr, io : Io) -> Impl : (Future[Future](TcpStream) IoExn : IoExn)Connect to a remote address, returning a new TcpStream.
Parameters
| Name | Type | Notes |
|---|---|---|
addr | SocketAddr | |
io | Io |
read : (TcpStream) fn(self : TcpStream, buf : *(u8), size : usize, io : Io) -> Impl : (Future[Future](usize) IoExn : IoExn)Read up to size bytes from the stream into buf, resolving to the
number actually read. 0 — and only 0 — means the peer closed its end;
a SHORT count is ordinary on a stream socket and says nothing about
end-of-stream, so a caller that needs size bytes loops. Awaiting parks
this task on the event loop until the kernel has bytes; a failure throws
IoExn rather than resolving to a negative count.
buf must have room for size bytes, and raw pointers are unavailable
in safe code, so calling this directly needs
pragma(Pragma.AllowUnsafe). Safe code uses the Reader defaults
(read_to_end, read_to_string) or wraps the stream in
BufReader(TcpStream).
Parameters
| Name | Type | Notes |
|---|---|---|
self | TcpStream | |
buf | *(u8) | |
size | usize | |
io | Io |
write : (TcpStream) fn(self : TcpStream, buf : *(u8), size : usize, io : Io) -> Impl : (Future[Future](usize) IoExn : IoExn)Write up to size raw bytes from buf, resolving to the number the
kernel accepted — which may be LESS than size. A caller that must
deliver everything loops, or takes the Writer trait's write_all.
Awaiting parks this task until the socket is writable; a failure throws
IoExn. Needs pragma(Pragma.AllowUnsafe) for the raw pointer —
write_bytes / write_str / write_string are the safe spellings.
Parameters
| Name | Type | Notes |
|---|---|---|
self | TcpStream | |
buf | *(u8) | |
size | usize | |
io | Io |
write_str : (TcpStream) fn(self : TcpStream, data : str, io : Io) -> Impl : (Future[Future](usize) IoExn : IoExn)write_string : (TcpStream) fn(self : TcpStream, data : String, io : Io) -> Impl : (Future[Future](usize) IoExn : IoExn)write_bytes : (TcpStream) fn(self : TcpStream, data : ArrayList(u8), io : Io) -> Impl : (Future[Future](usize) IoExn : IoExn)shutdown : (TcpStream) fn(self : TcpStream, how : Shutdown, io : Io) -> Impl : (Future[Future](unit) IoExn : IoExn)close : (TcpStream) fn(self : TcpStream, io : Io) -> Impl : (Future[Future](unit) IoExn : IoExn)peer_addr : (TcpStream) fn(self : TcpStream) -> SocketAddrlocal_addr : (TcpStream) fn(self : TcpStream) -> SocketAddrGet this end's socket address — Rust's TcpStream::local_addr. Read
from the kernel once, when the stream was connected or accepted, because
that is the only way to learn the ephemeral port and the source
interface the kernel chose; neither is in the connect argument.
Parameters
| Name | Type | Notes |
|---|---|---|
self | TcpStream |
Returns: SocketAddr
fd : (TcpStream) fn(self : TcpStream) -> i32set_nodelay : (TcpStream) fn(self : TcpStream, nodelay : bool, io : Io) -> Impl : (Future[Future](unit) IoExn : IoExn)impl(TcpStream, Dispose(...))
dispose : (TcpStream) fn(self : TcpStream) -> unitRelease the resources self owns — a file descriptor, a socket, a lock,
a buffer the allocator handed out. Called automatically when the last
reference to the value goes away, so an implementor never calls it
directly and must tolerate being the only one who ever does.
It must be safe to run exactly once: the runtime calls it at refcount
zero, and a type that also exposes an explicit close/release is
responsible for making the second call a no-op.
Parameters
| Name | Type | Notes |
|---|---|---|
self | TcpStream |
Returns: unit
impl(TcpStream, IoTraits)
impl(TcpStream, IoTraits)
Methods
read_to_end : (TcpStream) fn(self : TcpStream, io : Io) -> Impl : (Future[Future](ArrayList(u8)) IoExn : IoExn)read_to_string : (TcpStream) fn(self : TcpStream, io : Io) -> Impl : (Future[Future](String) IoExn : IoExn)flush : (TcpStream) fn(self : TcpStream, io : Io) -> Impl : (Future[Future](unit) IoExn : IoExn)Functions
Convert a SocketAddr to a low-level SockAddr buffer.
Shared with std/net/udp, which used to carry a byte-for-byte COPY of this
— which is how the hardcoded-"::1" bug below had to be fixed twice. One
encoder and one decoder (sockaddr_to_socket_addr) for the whole net
layer.
Parameters
| Name | Type | Notes |
|---|---|---|
addr | SocketAddr |
Returns: IO_tcp.SockAddr
Parse a low-level sockaddr buffer (as filled in by accept/getsockname/
recvfrom) back into a SocketAddr. Unknown families fall back to 0.0.0.0:0.
Shared with std/net/udp (its bind readback) — one decoder for the whole
net layer.
Parameters
| Name | Type | Notes |
|---|---|---|
buf | *u8 |
Returns: SocketAddr