Python socket error handling: every exception and errno (tested)
Every Python socket error is an OSError, but they don't all carry useful errnos and they don't share the subclasses you'd expect. We caused the six failures people search for most (refused, DNS, timeout, reset, address in use, closed socket), logged exactly what Python raised, and built a handler that sorts them correctly.
1. The socket exception hierarchy
socket.error is just an old name for OSError, and socket.timeout has been an alias of the built-in TimeoutError since Python 3.10. So everything a socket raises is an OSError:
import socket
for name in ["error", "timeout", "gaierror", "herror"]:
cls = getattr(socket, name)
print(f"socket.{name:9} -> {cls.__name__:13} mro: {' > '.join(c.__name__ for c in cls.__mro__[:-2])}")
for cls in [ConnectionRefusedError, ConnectionResetError, ConnectionAbortedError, BrokenPipeError]:
print(f"{cls.__name__:23} mro: {' > '.join(c.__name__ for c in cls.__mro__[:-2])}")
Output:
socket.error -> OSError mro: OSError > Exception
socket.timeout -> TimeoutError mro: TimeoutError > OSError > Exception
socket.gaierror -> gaierror mro: gaierror > OSError > Exception
socket.herror -> herror mro: herror > OSError > Exception
ConnectionRefusedError mro: ConnectionRefusedError > ConnectionError > OSError > Exception
ConnectionResetError mro: ConnectionResetError > ConnectionError > OSError > Exception
ConnectionAbortedError mro: ConnectionAbortedError > ConnectionError > OSError > Exception
BrokenPipeError mro: BrokenPipeError > ConnectionError > OSError > Exception
The four ConnectionError subclasses are the "the other side went away" family. DNS failures (gaierror) and timeouts are not ConnectionErrors, so except ConnectionError on its own misses both.
2. What each common failure actually raises
We caused six common socket failures against local sockets and printed exactly what Python raised:
import socket, struct, threading, time
def show(label, fn):
try:
fn()
print(f"{label:28} no exception")
except Exception as e:
print(f"{label:28} {type(e).__name__}: errno={getattr(e, 'errno', None)} {e}")
# 1. nothing listening on the port
def refused():
s = socket.socket(); s.bind(("127.0.0.1", 0)); port = s.getsockname()[1]; s.close()
socket.create_connection(("127.0.0.1", port), timeout=2)
show("connect, nothing listening", refused)
# 2. hostname that cannot resolve
show("connect, bad hostname", lambda: socket.create_connection(("no-such-host.invalid", 80), timeout=2))
# 3. server accepts but never sends
srv = socket.create_server(("127.0.0.1", 0)); port = srv.getsockname()[1]
def silent():
c = socket.create_connection(("127.0.0.1", port), timeout=0.5)
c.recv(1024)
show("recv, server silent", silent)
# 4. server aborts the connection (SO_LINGER 0 -> RST)
def rst_server():
conn, _ = srv.accept(); conn.recv(1) # drain the silent client's connection
conn, _ = srv.accept()
conn.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0))
conn.close()
threading.Thread(target=rst_server, daemon=True).start()
def reset():
c = socket.create_connection(("127.0.0.1", port), timeout=2)
time.sleep(0.3)
c.recv(1024)
show("recv, server sent RST", reset)
# 5. port already bound
def in_use():
a = socket.socket(); a.bind(("127.0.0.1", 0)); a.listen()
b = socket.socket(); b.bind(a.getsockname())
show("bind, port in use", in_use)
# 6. using a socket after close()
def closed():
s = socket.socket(); s.close(); s.send(b"x")
show("send after close()", closed)
Output:
connect, nothing listening ConnectionRefusedError: errno=111 [Errno 111] Connection refused
connect, bad hostname gaierror: errno=-2 [Errno -2] Name or service not known
recv, server silent TimeoutError: errno=None timed out
recv, server sent RST ConnectionResetError: errno=104 [Errno 104] Connection reset by peer
bind, port in use OSError: errno=98 [Errno 98] Address already in use
send after close() OSError: errno=9 [Errno 9] Bad file descriptor
Things to notice:
- Timeouts have
errno=None. Code that branches one.errnogetsNonefor a timeout, so checkisinstance(e, TimeoutError)instead. - "Address already in use" is a plain
OSErrorwith errno 98 (EADDRINUSE). It has no subclass, so you can only catch it by checking the errno. See our EADDRINUSE guide for finding the process holding the port. - Using a socket after
close()gives[Errno 9] Bad file descriptor. That is a bug in your code, not a network problem (errno 9 guide). - On Windows the same exception classes appear with Winsock codes instead, for example
[WinError 10061]for refused and[WinError 10054]for reset. Catch the classes, not the numbers, and your code works on both.
Deep dives: ConnectionRefusedError errno 111, BrokenPipeError errno 32, BlockingIOError errno 11, the OSError errno list.
3. A handler that sorts them correctly (and the gaierror errno trap)
Order matters: the specific classes first, then the errno checks for the plain OSErrors:
import errno, socket
def classify(e):
if isinstance(e, socket.gaierror):
return f"DNS failure ({e.errno}: {e.strerror})"
if isinstance(e, TimeoutError):
return "timed out"
if isinstance(e, ConnectionError):
return f"connection problem: {type(e).__name__}"
if e.errno == errno.EADDRINUSE:
return "port already in use"
return f"other OSError {errno.errorcode.get(e.errno, e.errno)}"
errs = [socket.gaierror(-2, "Name or service not known"),
TimeoutError("timed out"),
ConnectionRefusedError(111, "Connection refused"),
OSError(98, "Address already in use"),
OSError(9, "Bad file descriptor")]
for e in errs:
print(f"{type(e).__name__:24} -> {classify(e)}")
print()
print("gaierror -2 in errno.errorcode:", -2 in errno.errorcode)
print("gaierror -2 == socket.EAI_NONAME:", -2 == socket.EAI_NONAME)
print("errno.errorcode[2] is", errno.errorcode[2], "(a different error)")
Output:
gaierror -> DNS failure (-2: Name or service not known)
TimeoutError -> timed out
ConnectionRefusedError -> connection problem: ConnectionRefusedError
OSError -> port already in use
OSError -> other OSError EBADF
gaierror -2 in errno.errorcode: False
gaierror -2 == socket.EAI_NONAME: True
errno.errorcode[2] is ENOENT (a different error)
The trap: gaierror errnos are negative EAI_* codes from the resolver, not errno values. -2 is socket.EAI_NONAME and is not in errno.errorcode at all. Writing abs(e.errno) or a lookup table that mixes the two gives you ENOENT, a file error that has nothing to do with DNS. Compare gaierror codes against socket.EAI_* constants and keep them apart from the others.
4. When the other side closes: no exception, then BrokenPipeError
A peer that closes normally does not raise anything on your side, and your first write after that still succeeds:
import socket, time
srv = socket.create_server(("127.0.0.1", 0))
cli = socket.create_connection(srv.getsockname())
conn, _ = srv.accept()
conn.close() # peer closes normally (FIN)
time.sleep(0.1)
print("recv after peer close:", cli.recv(1024)) # no exception
for i in range(1, 4):
try:
n = cli.send(b"hello")
print(f"send #{i}: sent {n} bytes, no error")
except OSError as e:
print(f"send #{i}: {type(e).__name__}: {e}")
time.sleep(0.1)
Output:
recv after peer close: b''
send #1: sent 5 bytes, no error
send #2: BrokenPipeError: [Errno 32] Broken pipe
send #3: BrokenPipeError: [Errno 32] Broken pipe
recv() returning b"" is how a socket says "the other side closed cleanly". A loop like while True: data = s.recv(1024) that never checks for empty bytes will spin forever. The first send() after the close still "worked" (it went into the kernel buffer, and the peer answered with a reset), so the error only appeared on the second send. That is why a write failure often shows up one call later than the real problem. Check if not data: break after every recv(), and treat BrokenPipeError and ConnectionResetError on writes as "peer is gone".
Frequently asked questions
What exceptions does a Python socket raise?
Everything is an OSError. The useful subclasses are ConnectionRefusedError, ConnectionResetError, ConnectionAbortedError and BrokenPipeError (all ConnectionError), TimeoutError (socket.timeout) and socket.gaierror for DNS failures.
Is socket.error still used?
socket.error is an alias of OSError. Use except OSError, or a more specific subclass.
Why is e.errno None on a socket timeout?
socket timeouts raise TimeoutError("timed out") with no errno. Check isinstance(e, TimeoutError) instead of the errno.
How do I catch "Address already in use" in Python?
It is a plain OSError with errno 98 (errno.EADDRINUSE) on Linux. Catch OSError and compare e.errno with errno.EADDRINUSE.
What does socket.gaierror [Errno -2] mean?
Name or service not known: the hostname could not be resolved. -2 is socket.EAI_NONAME, a resolver code, not an errno value.
Written with AI assistance. Measured on 2026-10-22 on Debian with Python 3.13.5, using local sockets only: connecting to a closed port, resolving a .invalid hostname, reading from a silent server, a server that closes with SO_LINGER 0, binding a port twice, using a closed socket, and writing after the peer closed. Every block was run verbatim and its output pasted unedited. PlainHeaders is our extension; Python is free.