diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for network-unexceptional
+
+## 0.1.0.0 -- YYYY-mm-dd
+
+* First version.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2023, Andrew Martin
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Andrew Martin nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/network-unexceptional.cabal b/network-unexceptional.cabal
new file mode 100644
--- /dev/null
+++ b/network-unexceptional.cabal
@@ -0,0 +1,27 @@
+cabal-version: 3.0
+name: network-unexceptional
+version: 0.1.0.0
+synopsis: Network functions that do not throw exceptions
+license: BSD-3-Clause
+license-file: LICENSE
+author: Andrew Martin
+maintainer: andrew.thaddeus@gmail.com
+copyright: 2023 Andrew Martin
+build-type: Simple
+extra-doc-files: CHANGELOG.md
+
+library
+  ghc-options: -O2 -Wall
+  exposed-modules:
+    Network.Unexceptional.Bytes
+    Network.Unexceptional.Chunks
+    Network.Unexceptional.MutableBytes
+  build-depends:
+    , base >=4.17 && <5
+    , posix-api >=0.6.0.1
+    , error-codes >=0.1.1
+    , byteslice >=0.2.8
+    , network >=3.1
+    , primitive >=0.8
+  hs-source-dirs: src
+  default-language: Haskell2010
diff --git a/src/Network/Unexceptional/Bytes.hs b/src/Network/Unexceptional/Bytes.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Unexceptional/Bytes.hs
@@ -0,0 +1,68 @@
+{-# language BangPatterns #-}
+{-# language DuplicateRecordFields #-}
+{-# language PatternSynonyms #-}
+{-# language LambdaCase #-}
+{-# language NamedFieldPuns #-}
+
+module Network.Unexceptional.Bytes
+  ( send
+  , receive
+  ) where
+
+import System.Posix.Types (Fd(Fd))
+import Foreign.C.Error (Errno)
+import Foreign.C.Types (CInt)
+import Network.Socket (Socket)
+import Data.Bytes.Types (Bytes(Bytes),MutableBytes(MutableBytes))
+import Foreign.C.Error.Pattern (pattern EWOULDBLOCK,pattern EAGAIN)
+import Data.Array.Byte (ByteArray)
+import GHC.Conc (threadWaitRead, threadWaitWrite)
+import Control.Monad (when)
+
+import qualified Posix.Socket as X
+import qualified Linux.Socket as X
+import qualified Data.Bytes.Types
+import qualified Network.Socket as S
+import qualified Data.Primitive as PM
+import qualified Network.Unexceptional.MutableBytes as MB
+
+-- | Send the entire byte sequence. This call POSIX @send@ in a loop
+-- until all of the bytes have been sent.
+send ::
+     Socket
+  -> Bytes
+  -> IO (Either Errno ())
+send s Bytes{array,offset,length=len} = S.withFdSocket s $ \fd ->
+  -- We attempt the first send without testing if the socket is in
+  -- ready for writes. This is because it is uncommon for the transmit
+  -- buffer to already be full.
+  sendLoop (Fd fd) array offset len
+
+-- does not wait for file descriptor to be ready
+sendLoop :: Fd -> ByteArray -> Int -> Int -> IO (Either Errno ())
+sendLoop !fd !arr !off !len =
+  X.uninterruptibleSendByteArray fd arr off (fromIntegral len) (X.noSignal <> X.dontWait) >>= \case
+    Left e -> if e == EAGAIN || e == EWOULDBLOCK
+      then do
+        threadWaitWrite fd
+        sendLoop fd arr off len
+      else pure (Left e)
+    Right sentSzC ->
+      let sentSz = fromIntegral sentSzC :: Int
+       in case compare sentSz len of
+            EQ -> pure (Right ())
+            LT -> sendLoop fd arr (off + sentSz) (len - sentSz)
+            GT -> fail "Network.Unexceptional.Bytes.sendAll: send claimed to send too many bytes"
+
+receive :: 
+     Socket
+  -> Int -- ^ Maximum number of bytes to receive
+  -> IO (Either Errno Bytes)
+receive s n = do
+  dst <- PM.newByteArray n
+  MB.receive s (MutableBytes dst 0 n) >>= \case
+    Left e -> pure (Left e)
+    Right m -> do
+      when (m < n) (PM.shrinkMutableByteArray dst m)
+      dst' <- PM.unsafeFreezeByteArray dst 
+      pure (Right (Bytes dst' 0 m))
diff --git a/src/Network/Unexceptional/Chunks.hs b/src/Network/Unexceptional/Chunks.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Unexceptional/Chunks.hs
@@ -0,0 +1,24 @@
+{-# language BangPatterns #-}
+{-# language DuplicateRecordFields #-}
+{-# language PatternSynonyms #-}
+{-# language LambdaCase #-}
+{-# language NamedFieldPuns #-}
+
+module Network.Unexceptional.Chunks
+  ( send
+  ) where
+
+import Foreign.C.Error (Errno)
+import Network.Socket (Socket)
+import Data.Bytes.Chunks (Chunks)
+
+import qualified Data.Bytes.Chunks as Chunks
+import qualified Network.Unexceptional.Bytes as NB
+
+-- | Send the entire byte sequence. This call POSIX @send@ in a loop
+-- until all of the bytes have been sent.
+send ::
+     Socket
+  -> Chunks
+  -> IO (Either Errno ())
+send !s cs = NB.send s (Chunks.concat cs)
diff --git a/src/Network/Unexceptional/MutableBytes.hs b/src/Network/Unexceptional/MutableBytes.hs
new file mode 100644
--- /dev/null
+++ b/src/Network/Unexceptional/MutableBytes.hs
@@ -0,0 +1,52 @@
+{-# language BangPatterns #-}
+{-# language DuplicateRecordFields #-}
+{-# language PatternSynonyms #-}
+{-# language LambdaCase #-}
+{-# language NamedFieldPuns #-}
+
+module Network.Unexceptional.MutableBytes
+  ( receive
+  ) where
+
+import Data.Bytes.Types (MutableBytes(MutableBytes))
+import Data.Primitive (MutableByteArray)
+import Foreign.C.Error (Errno)
+import Foreign.C.Error.Pattern (pattern EWOULDBLOCK,pattern EAGAIN)
+import GHC.Conc (threadWaitRead)
+import GHC.Exts (RealWorld)
+import Network.Socket (Socket)
+import System.Posix.Types (Fd(Fd))
+
+import qualified Data.Bytes.Types
+import qualified Linux.Socket as X
+import qualified Network.Socket as S
+import qualified Posix.Socket as X
+
+-- | Receive bytes from a socket. Receives at most N bytes, where N
+-- is the size of the buffer. Returns the number of bytes that were
+-- actually received.
+receive ::
+     Socket
+  -> MutableBytes RealWorld -- ^ Slice of a buffer
+  -> IO (Either Errno Int)
+receive s MutableBytes{array,offset,length=len} = S.withFdSocket s $ \fd ->
+  -- We attempt the first send without testing if the socket is in
+  -- ready for writes. This is because it is uncommon for the transmit
+  -- buffer to already be full.
+  receiveLoop (Fd fd) array offset len
+
+-- Does not wait for file descriptor to be ready. Only performs
+-- a single successful recv syscall
+receiveLoop :: Fd -> MutableByteArray RealWorld -> Int -> Int -> IO (Either Errno Int)
+receiveLoop !fd !arr !off !len =
+  X.uninterruptibleReceiveMutableByteArray fd arr off (fromIntegral len) X.dontWait >>= \case
+    Left e -> if e == EAGAIN || e == EWOULDBLOCK
+      then do
+        threadWaitRead fd
+        receiveLoop fd arr off len
+      else pure (Left e)
+    Right recvSzC ->
+      let recvSz = fromIntegral recvSzC :: Int
+       in case compare recvSz len of
+            GT -> fail "Network.Unexceptional.Bytes.receive: recv claimed to receive too many bytes"
+            _ -> pure (Right recvSz)
