Skip to content
Effect 4.0 is here. One ecosystem. Zero dependencies.

DatagramSocket

Pull-based UDP sockets with scoped native handles.

Acquiring a reader opens and binds a socket owned by its scope. Only one reader is open at a time; another acquisition waits interruptibly for it to close. Use Effect.retry around a scoped consume loop to open a new socket on each retry.

Writes wait while no reader is open, so a send-only client must still acquire a reader.

Example Portable request/reply client

import { Effect } from "effect"
import { NetAddress } from "effect/net"
import { DatagramSocket } from "effect/socket"
const server = NetAddress.inetAddressFromIpStringUnsafe("127.0.0.1", 5353)
export const request = (payload: string) =>
Effect.gen(function*() {
const socket = yield* DatagramSocket.DatagramSocket
const reader = yield* socket.reader
const writer = yield* socket.writer
yield* writer.write({ payload, address: server })
const [response] = yield* reader.pull
return response.payload
}).pipe(Effect.scoped)
24 exports Since v4.0.0 Source

Constructors

Since v4.0.0

Creates a DatagramSocket from its reader and writer effects.

Signature

declare function make(options: {
readonly reader: Effect<Reader, DatagramSocketError, Scope>;
readonly writer: Effect<Writer, never, Scope>;
}): DatagramSocket

Builds a DatagramSocket over a backing socket, opening a new socket for each reader acquisition.

Details

open receives the event callbacks before the handle exists, so packets can be queued while it opens. Core owns the receive queue and its overflow strategy, reader exclusivity, sticky errors, lazy address parsing, the writer latch and destination formatting. The handle is closed when the reader's scope closes. If acquisition is interrupted while open is still running, the interruption returns at once, open is left to finish, and the handle it produces is closed. The next reader waits until that open has settled, so two native sockets never exist at once. An adopted acquire must therefore finish in bounded time.

open runs in the reader's scope, with the services of the fiber that built the socket, so an adopted acquire can use them and register finalizers that run when the reader closes. Where the fiber acquiring the reader has a service with the same tag, its own takes precedence.

Concurrent pulls are served in the order they started waiting.

onError receives errors with no write left to fail. It runs synchronously, and anything it throws is ignored.

Invalid receiveBuffer.capacity values cause a defect; use a positive safe integer.

Signature

declare function makeFromBackingSocket<R = never>(open: (events: BackingEvents) => Effect<BackingSocket, DatagramSocketError, R>, options?: {
readonly onError?: (error: DatagramSocketError) => void;
readonly receiveBuffer?: ReceiveBufferOptions;
}): Effect<DatagramSocket, never, Exclude<R, Scope>>

Errors

The native socket closed underneath the reader, or the reader's scope closed.

Signature

declare class DatagramSocketClosedError extends {
readonly _tag: "DatagramSocketClosedError";
} & YieldableError<this> {
constructor(...args: [props?: {
readonly _tag?: "DatagramSocketClosedError";
}, options?: MakeOptions]);
}

The error raised by DatagramSocket operations, wrapping a specific reason. Its cause and message come from the reason.

Signature

declare class DatagramSocketError extends {
readonly _tag: "DatagramSocketError";
readonly reason: DatagramSocketOpenError | DatagramSocketReadError | DatagramSocketWriteError | DatagramSocketClosedError | DatagramSocketUnsupportedError;
} & YieldableError<this> {
constructor(props: {
readonly reason: DatagramSocketOpenError | DatagramSocketReadError | DatagramSocketWriteError | DatagramSocketClosedError | DatagramSocketUnsupportedError;
});
readonly "~effect/socket/DatagramSocket/DatagramSocketError": "~effect/socket/DatagramSocket/DatagramSocketError";
message: string;
}

Schema for the union of DatagramSocketError reasons.

Signature

declare const DatagramSocketErrorReason: Union<readonly [typeof DatagramSocketOpenError, typeof DatagramSocketReadError, typeof DatagramSocketWriteError, typeof DatagramSocketClosedError, typeof DatagramSocketUnsupportedError]>

The union of DatagramSocketError reasons.

Signature

type DatagramSocketErrorReason = typeof DatagramSocketErrorReason.Type

Runtime type identifier attached to DatagramSocketError values.

Signature

declare const DatagramSocketErrorTypeId: "~effect/socket/DatagramSocket/DatagramSocketError"

Opening or binding the native socket, applying its options, or resolving a name failed.

Signature

declare class DatagramSocketOpenError extends {
readonly _tag: "DatagramSocketOpenError";
readonly cause: unknown;
readonly kind: "Unknown" | "PermissionDenied" | "AddressInUse" | "AddressNotAvailable";
} & YieldableError<this> {
constructor(...args: [props: {
readonly _tag?: "DatagramSocketOpenError";
readonly cause: unknown;
readonly kind: "Unknown" | "PermissionDenied" | "AddressInUse" | "AddressNotAvailable";
}, options?: MakeOptions]);
}

A receive failed, or an ICMP report arrived with no write left to fail.

Signature

declare class DatagramSocketReadError extends {
readonly _tag: "DatagramSocketReadError";
readonly cause: unknown;
readonly kind: "Unknown" | "PermissionDenied" | "MessageTooLarge" | "Unreachable" | "ConnectionRefused";
} & YieldableError<this> {
constructor(...args: [props: {
readonly _tag?: "DatagramSocketReadError";
readonly cause: unknown;
readonly kind: "Unknown" | "PermissionDenied" | "MessageTooLarge" | "Unreachable" | "ConnectionRefused";
}, options?: MakeOptions]);
}

The runtime lacks a capability, such as source-specific multicast on Deno.

Signature

declare class DatagramSocketUnsupportedError extends {
readonly _tag: "DatagramSocketUnsupportedError";
readonly capability: string;
readonly runtime: string;
} & YieldableError<this> {
constructor(...args: [props: {
readonly _tag?: "DatagramSocketUnsupportedError";
readonly capability: string;
readonly runtime: string;
}, options?: MakeOptions]);
message: string;
}

A send failed. address is the destination when it is known.

Signature

declare class DatagramSocketWriteError extends {
readonly _tag: "DatagramSocketWriteError";
readonly address?: InetAddress;
readonly cause: unknown;
readonly kind: "Unknown" | "PermissionDenied" | "MessageTooLarge" | "Unreachable" | "ConnectionRefused";
} & YieldableError<this> {
constructor(...args: [props: {
readonly _tag?: "DatagramSocketWriteError";
readonly address?: InetAddress;
readonly cause: unknown;
readonly kind: "Unknown" | "PermissionDenied" | "MessageTooLarge" | "Unreachable" | "ConnectionRefused";
}, options?: MakeOptions]);
}
Since v4.0.0

The normalized kind of a send or receive failure.

Signature

type IoErrorKind = "MessageTooLarge" | "Unreachable" | "ConnectionRefused" | "PermissionDenied" | "Unknown"

Guards

Returns true when a value is a DatagramSocketError.

Signature

declare function isDatagramSocketError(u: unknown): u is DatagramSocketError

Models

Since v4.0.0

A native host and port, parsed on demand.

Signature

interface BackingAddress {
readonly host: string;
readonly port: number;
}

BackingEvents

interface
Since v4.0.0

Callbacks installed before opening the backing socket; packets may arrive synchronously.

Details

onPacket queues a received packet. onReadError is a terminal receive error and onClose reports the native socket closing underneath the reader; both are sticky. onError forwards errors with no write left to fail, such as ICMP reports, to the user's onError option.

Each callback can be passed on its own, for example as a runtime's event listener.

Signature

interface BackingEvents {
readonly onClose: () => void;
readonly onError: (error: DatagramSocketError) => void;
readonly onPacket: (payload: Uint8Array, host: string, port: number) => void;
readonly onReadError: (error: DatagramSocketError) => void;
}

BackingSocket

interface
Since v4.0.0

Native transport contract used by platform adapters.

Details

address is the raw bound address, read once at open. scopeIds maps IPv6 zone names to scope IDs, for parsing named zones and formatting destinations. peer is the default destination, and connected marks a natively connected socket, whose sends receive no destination.

send and sendMany hand datagrams to the runtime and call done exactly once, synchronously or later, when the runtime reports the result. They must not throw. sendMany passes the index of the failing datagram to done when the runtime knows it, so core can attach its address.

Adapters normalize native errors before passing them to core. Read destination host and port during the call; do not retain the record.

close must not throw.

Signature

interface BackingSocket {
readonly address: BackingAddress;
readonly close: () => void;
readonly connected?: boolean;
readonly joinMulticast: <A extends IpAddress>(options: {
readonly group: MulticastAddress<A>;
readonly interface?: MulticastInterface<A>;
readonly source?: A;
}) => Effect<() => Effect<void, never>, DatagramSocketError>;
readonly peer?: BackingAddress;
readonly scopeIds?: ReadonlyMap<string, number>;
readonly send: (payload: Uint8Array, destination: BackingAddress | undefined, done: (error?: DatagramSocketError) => void) => void;
readonly sendMany: (payloads: readonly Array<Uint8Array<ArrayBufferLike>>, destinations: readonly Array<BackingAddress | undefined>, done: (error?: DatagramSocketError, index?: number) => void) => void;
}

Datagram

interface
Since v4.0.0

A received packet.

Details

The payload is safe to retain. The sender's address is parsed on first read and cached.

Reply with writer.write({ payload, address: received }) to reuse the raw sender address without parsing. This requires a datagram from a platform adapter or makeFromBackingSocket. For hand-built datagrams or custom readers, use { payload, address: received.address }.

Signature

interface Datagram {
readonly address: InetAddress;
readonly payload: Uint8Array;
}
Since v4.0.0

A UDP socket with a scoped, exclusive reader. Acquiring reader opens and binds a native socket; subsequent acquisitions wait for its scope to close. Acquiring writer cannot fail, but writes wait for an open reader.

Signature

interface DatagramSocket {
readonly "~effect/socket/DatagramSocket": "~effect/socket/DatagramSocket";
readonly reader: Effect<Reader, DatagramSocketError, Scope>;
readonly writer: Effect<Writer, never, Scope>;
}
Since v4.0.0

A packet to send.

Details

String payloads are encoded as UTF-8. address can be omitted when the socket has a peer or is connected. On a connected socket an explicit InetAddress fails the write, while a received Datagram is allowed and goes out on the connected path.

Pass a received Datagram as the destination to reply, or as the whole argument to echo it. Both reuse the raw sender address and require a datagram from a platform adapter or makeFromBackingSocket. Hand-built datagrams and custom readers must pass an explicit InetAddress instead.

Signature

interface OutgoingDatagram {
readonly address?: InetAddress | Datagram;
readonly payload: string | Uint8Array<ArrayBufferLike>;
}

Reader

interface
Since v4.0.0

The receive side of an open native socket.

Details

pull returns everything queued since the last pull, or waits for the next packet. After a terminal error, pull first returns the queued packets and then fails with the same error until the reader's scope closes. Closing the scope discards the queue and fails every waiting pull with DatagramSocketClosedError.

Concurrent pulls wait in FIFO order. Each incoming packet goes to the oldest waiter; a terminal error fails all waiters.

address is the bound local address, parsed on first read and cached.

dropped counts receive-buffer overflow since this reader opened, not kernel or network loss or packets discarded on scope close.

joinMulticast joins a group until its scope closes; leave failures are ignored. Its interface selects where packets arrive (ingress), not the platform's multicast.interface for sending (egress).

Signature

interface Reader {
readonly address: InetAddress;
readonly dropped: () => number;
readonly joinMulticast: <A extends IpAddress>(options: {
readonly group: MulticastAddress<A>;
readonly interface?: MulticastInterface<A>;
readonly source?: A;
}) => Effect<void, DatagramSocketError, Scope>;
readonly pull: Effect<readonly [Datagram, Datagram], DatagramSocketError>;
}
Since v4.0.0

Receive queue options.

Details

The queue is bounded in packets, with no byte cap. capacity defaults to 1024 and must be a positive safe integer. The worst-case memory is capacity ร— 64 KiB, about 64 MiB at the default.

When the queue is full, "dropping" (the default) discards the incoming packet and "sliding" discards the oldest queued one. Both count the packet in Reader.dropped.

Signature

interface ReceiveBufferOptions {
readonly capacity?: number;
readonly strategy?: "sliding" | "dropping";
}

Writer

interface
Since v4.0.0

The send side of a DatagramSocket. Writes wait until a reader is open, so a send-only client must still acquire a reader.

Details

A write completes when the runtime reports the send's result, and send errors fail it with a DatagramSocketWriteError carrying the destination address. After a terminal reader error, writes fail with that error until the reader's scope closes, and then wait for the next reader.

writeAll fails with the first error, without resending. Partial delivery is unspecified. The error has no packet index, and its address is absent on Bun.

There is no built-in write timeout; use Effect.timeout.

Signature

interface Writer {
readonly write: (datagram: OutgoingDatagram) => Effect<void, DatagramSocketError>;
readonly writeAll: (datagrams: readonly [OutgoingDatagram, OutgoingDatagram]) => Effect<void, DatagramSocketError>;
}

Services

Service tag for UDP socket transports.

Signature

declare const DatagramSocket: Context.Service<DatagramSocket, DatagramSocket>

Type IDs

Since v4.0.0

Runtime type identifier attached to DatagramSocket services.

Signature

declare const TypeId: "~effect/socket/DatagramSocket"