Table of Contents

Troubleshooting

Dials that hang, streams that never end, “no address”, “no relay”, and other first-hour failures.

Symptom-first. Each entry names what to check and which page explains the mechanism.

Connect fails immediately

Error Meaning Fix
iroh.ErrNoAddress the EndpointAddr had no path and no lookup service could resolve one add WithIP/WithRelayURL to the address, or register a resolver with WithAddressLookup
iroh.ErrNoRelay a relay path was required but no relay was usable check WithRelayMode; relay.ModeDisabled() removes relays entirely
iroh.ErrSelfConnect you dialed your own endpoint ID you are reusing one secret key for both sides
iroh.ErrConnectRejected / ErrHandshakeRejected the peer refused usually an ALPN mismatch — the dialed ALPN must be one the peer accepts
iroh.ErrEndpointClosed the endpoint was shut down a defer ep.Shutdown(ctx) that ran earlier than you expected
iroh.ErrEndpointAcceptLoopInUse two accept loops on one endpoint a Router already owns accepts; do not also call Accept

Connect hangs, then times out

A single Connect call is bounded by iroh.ConnectTimeout (10s) when no reachable address succeeds. If you consistently hit it:

  1. Check what path could possibly work. Two peers behind different NATs with only ip: addresses in their EndpointAddr have no path at all. Give the receiving side a relay: iroh.WithRelayMode(relay.ModeDefault()), then ep.Online(ctx) before publishing its address.
  2. Check the address was minted after the relay connected. ep.Addr() taken before Online may have no relay URL. That is the most common way a ticket ends up unreachable. Use ep.WatchAddr() if you re-publish.
  3. Check the ALPN strings match exactly. They are compared byte for byte.
  4. Check the clocks and the network. ep.NetReport() (with iroh.WithNetReport()) reports whether IPv4/IPv6 QAD round trips completed and whether your observed address varies by destination — the symmetric-NAT signal that predicts hole-punching failure.

See Relays and discovery.

The connection works but is slow

Check whether you are still on the relay path:

for _, p := range conn.Paths() {
	fmt.Println(p.ID, p.Validated, p.Addr, p.RTT)
}

A relay path is expected at connection setup; the direct path appears once hole punching succeeds. If a direct path never appears, the NAT topology may not permit one — that is a working configuration, just a slower one. WatchPaths lets you observe the upgrade instead of sampling for it. Background: How go-iroh works.

A stream never returns EOF

The reader is waiting for a half-close that never came. On a bidirectional stream, Close() closes your send side; the peer’s io.Copy or ReadAll cannot return until you call it. In the echo tutorial, the client writes, calls s.Close(), and only then reads.

Symmetrically, a handler that returns without closing its stream leaves the client blocked.

Nothing accepts my connections

Browser (js/wasm) builds compile but do not connect

Browser endpoints are relay-only: direct UDP QUIC, direct paths, and NAT traversal are not available in browser WebAssembly. Build with iroh.WithoutIPTransports() and a relay mode, and expect relay latency. See the wasmrelay experiment module for working demos.

go get or go build fails on the Go version

go-iroh’s go.mod declares go 1.26. Check go version, and check your go.mod’s own go directive is not older than what the toolchain requires.

Tests hang instead of failing

The live Rust interop gates are opt-in and depend on external binaries and network topology; a gate that cannot run should skip, not hang. If a local test hangs, suspect the half-close pattern above in the test itself. See Testing and interop.

Still stuck

The protocol-level questions — why hole punching failed, what the relay does, how discovery records are structured — are answered upstream at docs.iroh.computer and in n0-computer/iroh. Go API questions belong in github.com/tmc/go-iroh.

Last updated: 2026-08-20