Module net/tcp

net/tcp
Stability: unstable — may still change; see below.

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 — ONE shape is still open, 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. Freezing waits on that pointer-free read; the listener/stream names, which mirror Rust's, are not expected to move.

The second open shape is CLOSED: incoming exists (see Incoming below), built on the Stream trait in std/async/stream.yo, so a server loop can be a stream with combinators rather than a hand-rolled accept loop.

Types

Shutdown enum
Shutdown

Which half of a TCP connection to shut down — the argument to TcpStream.shutdown.

Variants

VariantFieldsDescription
Read

Further receives are disallowed (SHUT_RD).

Write

Further sends are disallowed (SHUT_WR).

Both

Both further sends and receives are disallowed (SHUT_RDWR).

Methods
clone : (Shutdown) fn(self : &Shutdown) -> Shutdown

Parameters

NameTypeNotes
selfShutdown

Returns: Shutdown

Incoming struct
Incoming

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 OWNS the listener (incoming moves it in, as tokio-stream's TcpListenerStream::new does): dropping the stream closes the socket. listener() lends it back, for local_addr or an early close. The stream finishes (.None) once the listener is closed.

Fields

NameTypeDescription
_listenerTcpListener

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))

Parameters

NameTypeNotes
selfS : (Stream)
fF : (Fn(A) -> B)

Returns: 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

NameTypeNotes
selfS : (Stream)
fF : (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

NameTypeNotes
selfS : (Stream)
fF : (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))

Parameters

NameTypeNotes
selfS : (Stream)
nusize

Returns: StreamTake(S : (Stream))

skip : fn(generic(A) self : S : (Stream), n : usize) -> StreamSkip(S : (Stream))

Parameters

NameTypeNotes
selfS : (Stream)
nusize

Returns: StreamSkip(S : (Stream))

for_each : fn(generic(A, F) self : S : (Stream), f : F : (Fn(A) -> unit), io : Io) -> Impl(Future(unit, Io))

Parameters

NameTypeNotes
selfS : (Stream)
fF : (Fn(A) -> unit)
ioIo

Returns: Impl(Future(unit, Io))

collect : fn(generic(A) self : S : (Stream), io : Io) -> Impl(Future(ArrayList(A), Io))

Parameters

NameTypeNotes
selfS : (Stream)
ioIo

Returns: Impl(Future(ArrayList(A), Io))

impl(Incoming, ...)
listener : (Incoming) fn(self : &Incoming) -> &TcpListener

The listener, lent.

Parameters

NameTypeNotes
selfIncoming

Returns: TcpListener

impl(Incoming, Stream(...))
next : (Incoming) fn(self : &mut Incoming, io : Io) -> Impl(Future(Option(Result(TcpStream, NetError)), Io))

Yield the next item, or .None once the stream is finished.

Parameters

NameTypeNotes
selfIncoming
ioIo

Returns: Impl(Future(Option(Result(TcpStream, NetError)), Io))

TcpListener struct
TcpListener

A TCP socket that listens for incoming connections.

A move-only value, as TcpStream is: one owner closes the socket, and a copy is E0901. Lend it with &listener; to serve from a spawned task, move the listener into the task, or share it as Rc(TcpListener).

Fields

NameTypeDescription
_cellBox(_TcpListenerState)
impl(TcpListener, ...)
bind : (TcpListener) fn(addr : SocketAddr, io : Io) -> Impl(Future(TcpListener, IoExn))

Bind to a socket address and start listening.

Parameters

NameTypeNotes
addrSocketAddr
ioIo

Returns: Impl(Future(TcpListener, IoExn))

accept : (TcpListener) fn(self : &TcpListener, io : Io) -> Impl(Future(TcpStream, IoExn))

Accept an incoming connection, returning a new TcpStream.

The future borrows the listener until it is awaited: the descriptor is read when the accept is submitted, so the listener cannot be dropped (closing it, and freeing the number for reuse) in between.

Parameters

NameTypeNotes
selfTcpListener
ioIo

Returns: Impl(Future(TcpStream, IoExn))

incoming : (TcpListener) fn(self : TcpListener) -> Incoming

The 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.

The stream takes the listener (it is moved in): read its address first, or through Incoming.listener().

Parameters

NameTypeNotes
selfTcpListener

Returns: Incoming

local_addr : (TcpListener) fn(self : &TcpListener) -> SocketAddr

Get the local address this listener is bound to.

Parameters

NameTypeNotes
selfTcpListener

Returns: SocketAddr

close : (TcpListener) fn(self : &TcpListener, io : Io) -> Impl(Future(unit, IoExn))

Close the listener socket.

Parameters

NameTypeNotes
selfTcpListener
ioIo

Returns: Impl(Future(unit, IoExn))

fd : (TcpListener) fn(self : &TcpListener) -> i32

Get the underlying file descriptor.

Parameters

NameTypeNotes
selfTcpListener

Returns: i32

TcpStream struct
TcpStream

A connected TCP stream for bidirectional data transfer.

A move-only value (its state is a Box): moving it moves the socket, and a copy is E0901. Lend it with &s, or share one stream as Rc(TcpStream) on one thread or Arc(Mutex(TcpStream)) across threads. Every method takes self : &Self; close marks the socket closed inside the box, so a TcpStream is never Sync.

Fields

NameTypeDescription
_cellBox(_TcpStreamState)

Trait Implementations

IoTraits
impl(TcpStream, ...)
connect : (TcpStream) fn(addr : SocketAddr, io : Io) -> Impl(Future(TcpStream, IoExn))

Connect to a remote address, returning a new TcpStream.

Parameters

NameTypeNotes
addrSocketAddr
ioIo

Returns: Impl(Future(TcpStream, IoExn))

write_str : (TcpStream) fn(self : &TcpStream, data : str, io : Io) -> Impl(Future(usize, IoExn))

Write a str to the stream. Returns the number of bytes written.

Parameters

NameTypeNotes
selfTcpStream
datastr
ioIo

Returns: Impl(Future(usize, IoExn))

write_string : (TcpStream) fn(self : &TcpStream, data : &String, io : Io) -> Impl(Future(usize, IoExn))

Write a String to the stream. Returns the number of bytes written.

Parameters

NameTypeNotes
selfTcpStream
dataString
ioIo

Returns: Impl(Future(usize, IoExn))

write_bytes : (TcpStream) fn(self : &TcpStream, data : &ArrayList(u8), io : Io) -> Impl(Future(usize, IoExn))

Write raw bytes from an ArrayList(u8) to the stream. Returns the number of bytes written.

Parameters

NameTypeNotes
selfTcpStream
dataArrayList(u8)
ioIo

Returns: Impl(Future(usize, IoExn))

shutdown : (TcpStream) fn(self : &TcpStream, how : Shutdown, io : Io) -> Impl(Future(unit, IoExn))

Shut down part or all of the connection.

Parameters

NameTypeNotes
selfTcpStream
howShutdown
ioIo

Returns: Impl(Future(unit, IoExn))

close : (TcpStream) fn(self : &TcpStream, io : Io) -> Impl(Future(unit, IoExn))

Close the stream.

Parameters

NameTypeNotes
selfTcpStream
ioIo

Returns: Impl(Future(unit, IoExn))

close_sync : (TcpStream) fn(self : &TcpStream) -> unit

Close the socket now, without the event loop: what Drop runs, for a caller that must release the descriptor from plain code before the stream itself goes away (a TLS stream's teardown, a connection pool's eviction). A no-op once the stream is closed. The destructor itself is never called by hand (E0915).

Parameters

NameTypeNotes
selfTcpStream

Returns: unit

peer_addr : (TcpStream) fn(self : &TcpStream) -> SocketAddr

Get the remote peer's socket address.

Parameters

NameTypeNotes
selfTcpStream

Returns: SocketAddr

local_addr : (TcpStream) fn(self : &TcpStream) -> SocketAddr

Get 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

NameTypeNotes
selfTcpStream

Returns: SocketAddr

fd : (TcpStream) fn(self : &TcpStream) -> i32

Get the underlying file descriptor.

Parameters

NameTypeNotes
selfTcpStream

Returns: i32

set_nodelay : (TcpStream) fn(self : &TcpStream, nodelay : bool, io : Io) -> Impl(Future(unit, IoExn))

Enable or disable TCP_NODELAY (disables Nagle's algorithm).

Parameters

NameTypeNotes
selfTcpStream
nodelaybool
ioIo

Returns: Impl(Future(unit, IoExn))

set_keepalive : (TcpStream) fn(self : &TcpStream, enabled : bool, io : Io) -> Impl(Future(unit, IoExn))

Enable or disable SO_KEEPALIVE on the socket.

Parameters

NameTypeNotes
selfTcpStream
enabledbool
ioIo

Returns: Impl(Future(unit, IoExn))

impl(TcpStream, IoTraits)
impl(TcpStream, IoTraits)
Methods
read : (TcpStream) fn(self : &TcpStream, buf : *(u8), size : usize, io : Io) -> Impl(Future(usize, 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

NameTypeNotes
selfTcpStream
buf*(u8)
sizeusize
ioIo

Returns: Impl(Future(usize, IoExn))

read_to_end : (TcpStream) fn(self : &TcpStream, io : Io) -> Impl(Future(ArrayList(u8), IoExn))

Parameters

NameTypeNotes
selfTcpStream
ioIo

Returns: Impl(Future(ArrayList(u8), IoExn))

read_to_string : (TcpStream) fn(self : &TcpStream, io : Io) -> Impl(Future(String, IoExn))

Parameters

NameTypeNotes
selfTcpStream
ioIo

Returns: Impl(Future(String, IoExn))

write : (TcpStream) fn(self : &TcpStream, buf : *(u8), size : usize, io : Io) -> Impl(Future(usize, 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

NameTypeNotes
selfTcpStream
buf*(u8)
sizeusize
ioIo

Returns: Impl(Future(usize, IoExn))

flush : (TcpStream) fn(self : &TcpStream, io : Io) -> Impl(Future(unit, IoExn))

Parameters

NameTypeNotes
selfTcpStream
ioIo

Returns: Impl(Future(unit, IoExn))

write_all : (TcpStream) fn(self : &TcpStream, buf : *(u8), size : usize, io : Io) -> Impl(Future(unit, IoExn))

Parameters

NameTypeNotes
selfTcpStream
buf*(u8)
sizeusize
ioIo

Returns: Impl(Future(unit, IoExn))

Functions

make_sockaddr function
fn(addr : SocketAddr) -> IO_tcp.SockAddr

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

NameTypeNotes
addrSocketAddr

Returns: IO_tcp.SockAddr

fn(buf : *u8) -> SocketAddr

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

NameTypeNotes
buf*u8

Returns: SocketAddr

fn(s : &TcpStream, io : Io) -> Impl(Future(unit, IoExn))

_close_stream_state over s's state, for a stream held inside another value's box (TlsStream): the future captures the state's address and borrows nothing, so the caller's own future must borrow the owner of s until this one completes.

Parameters

NameTypeNotes
s&TcpStream
ioIo

Returns: Impl(Future(unit, IoExn))