diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
 # Revision history for libfuse3
 
+## 0.1.2.0 -- 2020-11-09
+
+* Added `throwErrnoOf`, `tryErrno'` and `tryErrno_'` to `System.Libfuse3.Utils` (#5)
+* Added `ExceptionHandler` and `defaultExceptionHandler` (#6)
+* Fixed a bug in `resCFuseOperations` to prevent Haskell exceptions from escaping to C land (#7)
+* Added `pread` and `pwrite` to `System.Libfuse3.Utils` (#8)
+
 ## 0.1.1.1 -- 2020-10-06
 
 * Minor improvements on the documentations
diff --git a/bench/microbench/Main.hs b/bench/microbench/Main.hs
new file mode 100644
--- /dev/null
+++ b/bench/microbench/Main.hs
@@ -0,0 +1,40 @@
+module Main where
+
+import Control.Exception (bracket, onException)
+import Criterion.Main (bench, bgroup, defaultMain, nfIO)
+import Data.ByteString (ByteString)
+import Foreign (allocaBytes, free, mallocBytes)
+import System.LibFuse3.Utils
+import System.Posix.IO (OpenMode(ReadOnly), closeFd, defaultFileFlags, openFd)
+import System.Posix.Types (ByteCount, Fd(Fd), FileOffset)
+
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Unsafe as BU
+
+preadAlloca :: Fd -> ByteCount -> FileOffset -> IO ByteString
+preadAlloca (Fd fd) size off =
+  allocaBytes (fromIntegral size) $ \buf -> do
+    readBytes <- c_pread fd buf size off
+    B.packCStringLen (buf, fromIntegral readBytes)
+
+preadMalloc :: Fd -> ByteCount -> FileOffset -> IO ByteString
+preadMalloc (Fd fd) size off = do
+  buf <- mallocBytes (fromIntegral size)
+  let mkBS = do
+        readBytes <- c_pread fd buf size off
+        BU.unsafePackMallocCStringLen (buf, fromIntegral readBytes)
+  mkBS `onException` free buf
+
+main :: IO ()
+main = bracket
+  (openFd "/dev/zero" ReadOnly Nothing defaultFileFlags)
+  closeFd
+  $ \fd -> defaultMain
+    [ bgroup "pread"
+      [ bench "alloca" $ nfIO (preadAlloca fd size off)
+      , bench "malloc" $ nfIO (preadMalloc fd size off)
+      ]
+    ]
+  where
+  size = 1048576
+  off = 1024
diff --git a/configure b/configure
--- a/configure
+++ b/configure
@@ -1,6 +1,6 @@
 #! /bin/sh
 # Guess values for system-dependent variables and create Makefiles.
-# Generated by GNU Autoconf 2.69 for Haskell libfuse3 package 0.1.1.1.
+# Generated by GNU Autoconf 2.69 for Haskell libfuse3 package 0.1.2.0.
 #
 #
 # Copyright (C) 1992-1996, 1998-2012 Free Software Foundation, Inc.
@@ -576,8 +576,8 @@
 # Identity of this package.
 PACKAGE_NAME='Haskell libfuse3 package'
 PACKAGE_TARNAME='haskell-libfuse3-package'
-PACKAGE_VERSION='0.1.1.1'
-PACKAGE_STRING='Haskell libfuse3 package 0.1.1.1'
+PACKAGE_VERSION='0.1.2.0'
+PACKAGE_STRING='Haskell libfuse3 package 0.1.2.0'
 PACKAGE_BUGREPORT=''
 PACKAGE_URL=''
 
@@ -1192,7 +1192,7 @@
   # Omit some internal or obsolete options to make the list less imposing.
   # This message is too long to be a string in the A/UX 3.1 sh.
   cat <<_ACEOF
-\`configure' configures Haskell libfuse3 package 0.1.1.1 to adapt to many kinds of systems.
+\`configure' configures Haskell libfuse3 package 0.1.2.0 to adapt to many kinds of systems.
 
 Usage: $0 [OPTION]... [VAR=VALUE]...
 
@@ -1254,7 +1254,7 @@
 
 if test -n "$ac_init_help"; then
   case $ac_init_help in
-     short | recursive ) echo "Configuration of Haskell libfuse3 package 0.1.1.1:";;
+     short | recursive ) echo "Configuration of Haskell libfuse3 package 0.1.2.0:";;
    esac
   cat <<\_ACEOF
 
@@ -1341,7 +1341,7 @@
 test -n "$ac_init_help" && exit $ac_status
 if $ac_init_version; then
   cat <<\_ACEOF
-Haskell libfuse3 package configure 0.1.1.1
+Haskell libfuse3 package configure 0.1.2.0
 generated by GNU Autoconf 2.69
 
 Copyright (C) 2012 Free Software Foundation, Inc.
@@ -1453,7 +1453,7 @@
 This file contains any messages produced by compilers while
 running configure, to aid debugging if configure makes a mistake.
 
-It was created by Haskell libfuse3 package $as_me 0.1.1.1, which was
+It was created by Haskell libfuse3 package $as_me 0.1.2.0, which was
 generated by GNU Autoconf 2.69.  Invocation command line was
 
   $ $0 $@
@@ -3340,7 +3340,7 @@
 # report actual input values of CONFIG_FILES etc. instead of their
 # values after options handling.
 ac_log="
-This file was extended by Haskell libfuse3 package $as_me 0.1.1.1, which was
+This file was extended by Haskell libfuse3 package $as_me 0.1.2.0, which was
 generated by GNU Autoconf 2.69.  Invocation command line was
 
   CONFIG_FILES    = $CONFIG_FILES
@@ -3402,7 +3402,7 @@
 cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
 ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`"
 ac_cs_version="\\
-Haskell libfuse3 package config.status 0.1.1.1
+Haskell libfuse3 package config.status 0.1.2.0
 configured by $0, generated by GNU Autoconf 2.69,
   with options \\"\$ac_cs_config\\"
 
diff --git a/configure.ac b/configure.ac
--- a/configure.ac
+++ b/configure.ac
@@ -1,4 +1,4 @@
-AC_INIT([Haskell libfuse3 package], [0.1.1.1])
+AC_INIT([Haskell libfuse3 package], [0.1.2.0])
 
 # Safety check: Ensure that we are in the correct source directory.
 AC_CONFIG_SRCDIR([libfuse3.cabal])
diff --git a/example/passthrough.hs b/example/passthrough.hs
--- a/example/passthrough.hs
+++ b/example/passthrough.hs
@@ -19,7 +19,7 @@
 import Data.Function (fix)
 import Data.Time.Clock.POSIX (POSIXTime)
 import Data.Void (Void)
-import Foreign (Ptr, allocaBytes, with)
+import Foreign (Ptr, with)
 import Foreign.C (CInt(CInt), CSize(CSize), CUInt(CUInt), Errno(Errno), eIO, eOK, eOPNOTSUPP)
 import System.IO (SeekMode, hPrint, stderr)
 import System.LibFuse3
@@ -29,16 +29,8 @@
 import System.Posix.IO (OpenFileFlags, OpenMode(WriteOnly), closeFd, defaultFileFlags, exclusive, fdSeek, openFd)
 import System.Posix.Types (ByteCount, COff(COff), CSsize(CSsize), DeviceID, Fd(Fd), FileMode, FileOffset, GroupID, UserID)
 
-import qualified Data.ByteString as B
-import qualified Data.ByteString.Unsafe as BU
 import qualified System.LibFuse3.FuseConfig as FuseConfig
 
-foreign import ccall "pread"
-  c_pread :: CInt -> Ptr a -> CSize -> COff -> IO CSsize
-
-foreign import ccall "pwrite"
-  c_pwrite :: CInt -> Ptr a -> CSize -> COff -> IO CSsize
-
 foreign import ccall "posix_fallocate"
   c_posix_fallocate :: CInt -> COff -> COff -> IO CInt
 
@@ -125,18 +117,10 @@
 xmpOpen path mode flags = tryErrno $ openFd path mode Nothing flags
 
 xmpRead :: Fd -> ByteCount -> FileOffset -> IO (Either Errno ByteString)
-xmpRead (Fd fd) size offset = tryErrno $
-  -- instead of allocaBytes + packCStringLen, a pair of mallocBytes + unsafePackMallocCString
-  -- can be used to gain a small (~1%) decrease in running time
-  allocaBytes (fromIntegral size) $ \buf -> do
-    readBytes <- c_pread fd buf size offset
-    B.packCStringLen (buf, fromIntegral readBytes)
+xmpRead fd size offset = tryErrno $ pread fd size offset
 
 xmpWrite :: Fd -> ByteString -> FileOffset -> IO (Either Errno CInt)
-xmpWrite (Fd fd) bs offset = tryErrno $
-  BU.unsafeUseAsCStringLen bs $ \(buf, size) ->
-    -- truncate CSsize (ssize_t = 64bit) to CInt (32bit) because there is no other way
-    fromIntegral <$> c_pwrite fd buf (fromIntegral size) offset
+xmpWrite fd bs offset = tryErrno $ fromIntegral <$> pwrite fd bs offset
 
 xmpStatfs :: FilePath -> IO (Either Errno FileSystemStats)
 xmpStatfs = tryErrno . getFileSystemStats
diff --git a/example/statjson.hs b/example/statjson.hs
--- a/example/statjson.hs
+++ b/example/statjson.hs
@@ -46,8 +46,7 @@
 import Data.Bits ((.&.), (.|.), complement)
 import Data.ByteString (ByteString)
 import Data.Void (Void)
-import Foreign (Ptr, allocaBytes)
-import Foreign.C (CInt(CInt), CSize(CSize), Errno, eIO, eNOSYS, eOPNOTSUPP)
+import Foreign.C (Errno, eIO, eNOSYS, eOPNOTSUPP)
 import Numeric (showHex)
 import System.Clock (TimeSpec(TimeSpec))
 import System.Directory (doesPathExist, listDirectory, makeAbsolute)
@@ -57,16 +56,12 @@
 import System.IO (hPrint, stderr)
 import System.Posix.IO (OpenFileFlags, OpenMode(ReadOnly), closeFd, openFd)
 import System.Posix.Files (regularFileMode, stdFileMode)
-import System.Posix.Types (COff(COff), CSsize(CSsize), ByteCount, Fd(Fd), FileOffset)
+import System.Posix.Types (ByteCount, Fd, FileOffset)
 
 import qualified Data.Aeson as Aeson
-import qualified Data.ByteString as B
 import qualified Data.ByteString.Lazy as BL
 import qualified System.Clock
 
-foreign import ccall "pread"
-  c_pread :: CInt -> Ptr a -> CSize -> COff -> IO CSsize
-
 -- | An outcome of an individual operation in a layer.
 data Outcome a
   -- | The request is forwarded to the next layer.
@@ -107,10 +102,7 @@
       case mode of
         ReadOnly -> tryErrno $ fmap Respond $ openFd (srcDir <> path) mode Nothing flags
         _ -> pure $ Left eOPNOTSUPP
-  , layerRead = \(Fd fd) size offset -> tryErrno $
-      allocaBytes (fromIntegral size) $ \buf -> do
-        readBytes <- c_pread fd buf size offset
-        B.packCStringLen (buf, fromIntegral readBytes)
+  , layerRead = \fd size offset -> tryErrno $ pread fd size offset
   , layerRelease = closeFd
   }
 
diff --git a/libfuse3.cabal b/libfuse3.cabal
--- a/libfuse3.cabal
+++ b/libfuse3.cabal
@@ -3,7 +3,7 @@
 -- For further documentation, see http://haskell.org/cabal/users-guide/
 
 name:                libfuse3
-version:             0.1.1.1
+version:             0.1.2.0
 synopsis:            A Haskell binding for libfuse-3.x
 description:         Bindings for libfuse, the FUSE userspace reference implementation, of version 3.x. Compatible with Linux.
 -- bug-reports:
@@ -53,6 +53,17 @@
   default-language:    Haskell2010
   ghc-options:         -Wall -fdefer-typed-holes
 
+test-suite unittest
+  type:                exitcode-stdio-1.0
+  main-is:             Main.hs
+  build-depends:       libfuse3, base
+                     , hspec
+  -- other-modules:
+  -- other-extensions:
+  hs-source-dirs:      test/unittest
+  default-language:    Haskell2010
+  ghc-options:         -Wall -fdefer-typed-holes -threaded
+
 test-suite integtest
   type:                exitcode-stdio-1.0
   main-is:             Main.hs
@@ -67,6 +78,18 @@
   -- other-modules:
   -- other-extensions:
   hs-source-dirs:      test/integtest
+  default-language:    Haskell2010
+  ghc-options:         -Wall -fdefer-typed-holes -threaded
+
+benchmark microbench
+  type:                exitcode-stdio-1.0
+  main-is:             Main.hs
+  -- other-modules:
+  build-depends:       libfuse3, base
+                     , bytestring
+                     , criterion
+                     , unix
+  hs-source-dirs:      bench/microbench
   default-language:    Haskell2010
   ghc-options:         -Wall -fdefer-typed-holes -threaded
 
diff --git a/src/System/LibFuse3.hs b/src/System/LibFuse3.hs
--- a/src/System/LibFuse3.hs
+++ b/src/System/LibFuse3.hs
@@ -29,6 +29,8 @@
   , module System.LibFuse3.Utils
 
   , fuseMain
+  , ExceptionHandler
+  , defaultExceptionHandler
   )
   where
 
diff --git a/src/System/LibFuse3/Internal.hsc b/src/System/LibFuse3/Internal.hsc
--- a/src/System/LibFuse3/Internal.hsc
+++ b/src/System/LibFuse3/Internal.hsc
@@ -7,7 +7,7 @@
 module System.LibFuse3.Internal where
 
 import Control.Applicative ((<|>))
-import Control.Exception (Exception, bracket_, finally, handle)
+import Control.Exception (Exception, SomeException, bracket_, catch, finally, fromException, handle)
 import Control.Monad (unless, void)
 import Control.Monad.IO.Class (liftIO)
 import Control.Monad.Trans.Resource (ResourceT, runResourceT)
@@ -37,12 +37,12 @@
   , pokeByteOff
   , with
   )
-import Foreign.C (CInt(CInt), CString, Errno, eINVAL, eNOSYS, eOK, getErrno, peekCString, resetErrno, throwErrno, withCStringLen)
+import Foreign.C (CInt(CInt), CString, Errno, eFAULT, eINVAL, eIO, eNOSYS, eOK, getErrno, peekCString, resetErrno, throwErrno, withCStringLen)
 import GHC.IO.Handle (hDuplicateTo)
 import System.Clock (TimeSpec)
 import System.Environment (getArgs, getProgName)
 import System.Exit (ExitCode(ExitFailure, ExitSuccess), exitFailure, exitSuccess, exitWith)
-import System.IO (IOMode(ReadMode, WriteMode), SeekMode(AbsoluteSeek, RelativeSeek, SeekFromEnd), stderr, stdin, stdout, withFile)
+import System.IO (IOMode(ReadMode, WriteMode), SeekMode(AbsoluteSeek, RelativeSeek, SeekFromEnd), hPutStrLn, stderr, stdin, stdout, withFile)
 import System.LibFuse3.FileStat (FileStat)
 import System.LibFuse3.FileSystemStats (FileSystemStats)
 import System.LibFuse3.FuseConfig (FuseConfig, fromCFuseConfig, toCFuseConfig)
@@ -395,21 +395,23 @@
 --
 -- The created `C.FuseOperations` has the following invariants:
 --
---   - The content of @fuse_operations.fh@ is a Haskell value of type @StablePtr fh@ or
+--   - The content of @fuse_file_info.fh@ is a Haskell value of type @StablePtr fh@ or
 --     @StablePtr dh@, depending on operations. It is created with `newFH`, accessed with
 --     `getFH` and released with `delFH`.
 --
---   - Every methods handle Haskell exception with the supplied error handler.
+--   - Every methods handle Haskell exception with the supplied error handler. Any exceptions
+--     not catched by it are catched, logged and returns `eIO`. This means that `exitSuccess`
+--     /does not work/ inside `FuseOperations`.
 --
 --   - NULL filepaths (passed from libfuse if `FuseConfig.nullpathOk` is set) are
 --     translated to empty strings.
 resCFuseOperations
   :: forall fh dh e
    . Exception e
-  => FuseOperations fh dh           -- ^ A set of file system operations.
-  -> (e -> IO Errno)                -- ^ An error handler that converts a Haskell exception to @errno@.
+  => FuseOperations fh dh
+  -> ExceptionHandler e
   -> ResourceT IO (Ptr C.FuseOperations)
-resCFuseOperations ops handler = do
+resCFuseOperations ops handlerRaw = do
   fuseGetattr       <- resC C.mkGetattr       wrapGetattr       (fuseGetattr ops)
   fuseReadlink      <- resC C.mkReadlink      wrapReadlink      (fuseReadlink ops)
   fuseMknod         <- resC C.mkMknod         wrapMknod         (fuseMknod ops)
@@ -457,6 +459,12 @@
     , ..
     }
   where
+  -- wraps the supplied handler to make sure no Haskell exceptions are propagated to the C land
+  handler :: ExceptionHandler SomeException
+  handler se = case fromException se of
+    Nothing -> defaultExceptionHandler se
+    Just e -> handlerRaw e `catch` defaultExceptionHandler
+
   -- convert a Haskell function to C one with @wrapMeth@, get its @FunPtr@, and associate it with freeHaskellFunPtr
   resC :: (cfunc -> IO (FunPtr cfunc)) -> (hsfunc -> cfunc) -> Maybe hsfunc -> ResourceT IO (FunPtr cfunc)
   resC _ _ Nothing = pure nullFunPtr
@@ -792,6 +800,9 @@
           _ -> Left eINVAL
     either (pure . Left) (go filePath fh offset) emode
 
+  _dummyToSuppressWarnings :: StablePtr a
+  _dummyToSuppressWarnings = error "dummy" eNOSYS
+
 -- | Allocates a @fuse_args@ struct to hold commandline arguments.
 resFuseArgs :: String -> [String] -> ResourceT IO (Ptr C.FuseArgs)
 resFuseArgs prog args = do
@@ -937,7 +948,7 @@
         else exitFailure
 
 -- | Parses the commandline arguments and runs fuse.
-fuseRun :: Exception e => String -> [String] -> FuseOperations fh dh -> (e -> IO Errno) -> IO a
+fuseRun :: Exception e => String -> [String] -> FuseOperations fh dh -> ExceptionHandler e -> IO a
 fuseRun prog args ops handler = runResourceT $ do
   pArgs <- resFuseArgs prog args
   mainArgs <- liftIO $ fuseParseCommandLineOrExit pArgs
@@ -954,7 +965,7 @@
 -- This is all that has to be called from the @main@ function. On top of
 -- the `FuseOperations` record with filesystem implementation, you must give
 -- an exception handler converting Haskell exceptions to `Errno`.
-fuseMain :: Exception e => FuseOperations fh dh -> (e -> IO Errno) -> IO ()
+fuseMain :: Exception e => FuseOperations fh dh -> ExceptionHandler e -> IO ()
 fuseMain ops handler = do
   -- this used to be implemented using libfuse's fuse_main. Doing this will fork()
   -- from C behind the GHC runtime's back, which deadlocks in GHC 6.8.
@@ -964,6 +975,22 @@
   args <- getArgs
   fuseRun prog args ops handler
 
+-- | An exception handler which converts Haskell exceptions from `FuseOperations` methods to `Errno`.
+type ExceptionHandler e = e -> IO Errno
+
+-- | Catches any exception, logs it to stderr, and returns `eIO`.
+--
+-- Suitable as a default exception handler.
+--
+-- __NOTE 1__ This differs from the one in the @HFuse@ package which returns `eFAULT`.
+--
+-- __NOTE 2__ If the filesystem is daemonized (as default), the exceptions will not be logged because
+-- stderr is redirected to @\/dev\/null@.
+defaultExceptionHandler :: ExceptionHandler SomeException
+defaultExceptionHandler e = hPutStrLn stderr (show e) >> pure eIO
+  where
+  _dummyToSuppressWarnings = error "dummy" eFAULT
+
 -- | Gets a file handle from `C.FuseFileInfo` which is embedded with `newFH`.
 --
 -- If either the @Ptr `C.FuseFileInfo`@ itself or its @fh@ field is NULL, returns @Nothing@.
@@ -1018,7 +1045,3 @@
 -- | Materializes the callback of @readdir@ to marshal `fuseReaddir`.
 foreign import ccall "dynamic"
   peekFuseFillDir :: FunPtr C.FuseFillDir -> C.FuseFillDir
-
--- | the dummy to please both ghc and haddock; don't use
-_dummy :: StablePtr a -> b
-_dummy _ = undefined eNOSYS
diff --git a/src/System/LibFuse3/Utils.hs b/src/System/LibFuse3/Utils.hs
--- a/src/System/LibFuse3/Utils.hs
+++ b/src/System/LibFuse3/Utils.hs
@@ -6,8 +6,11 @@
     testBitSet
 
   , -- * Errno
-    unErrno, ioErrorToErrno, tryErrno, tryErrno_
+    unErrno, ioErrorToErrno, throwErrnoOf, tryErrno, tryErrno_, tryErrno', tryErrno_'
 
+  , -- * File I/O
+    pread, pwrite, c_pread, c_pwrite
+
   , -- * Marshalling strings
     pokeCStringLen0
 
@@ -16,20 +19,24 @@
   )
   where
 
-import Control.Exception (tryJust)
+import Control.Exception (SomeException, try, tryJust)
 import Data.Bits ((.&.), Bits)
+import Data.ByteString (ByteString)
 import Data.Ratio ((%))
 import Data.Time.Clock.POSIX (POSIXTime)
-import Foreign (copyArray, pokeElemOff)
-import Foreign.C (CInt, CStringLen, Errno(Errno), eOK, errnoToIOError, throwErrno, withCStringLen)
+import Foreign (Ptr, allocaBytes, copyArray, pokeElemOff)
+import Foreign.C (CInt(CInt), CSize(CSize), CStringLen, Errno(Errno), eIO, eOK, errnoToIOError, getErrno, throwErrno, throwErrnoIfMinus1, withCStringLen)
 import GHC.IO.Exception (IOException(IOError, ioe_errno))
 import System.Clock (TimeSpec)
+import System.Posix.Types (ByteCount, COff(COff), CSsize(CSsize), Fd(Fd), FileOffset)
 
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Unsafe as BU
 import qualified System.Clock as TimeSpec
 
--- to have haddock link to proper entities
-_dummy :: dummy
-_dummy = error "dummy" errnoToIOError throwErrno
+-- | Identical to @extra@'s @try_@
+try_ :: IO a -> IO (Either SomeException a)
+try_ = try
 
 -- | Unwraps the newtype `Errno`.
 unErrno :: Errno -> CInt
@@ -41,6 +48,21 @@
 ioErrorToErrno IOError{ioe_errno=Just e} = Just $ Errno e
 ioErrorToErrno _ = Nothing
 
+-- | Like `throwErrno` but takes an `Errno` as a parameter instead of reading from `getErrno`.
+--
+-- This is an inverse of `tryErrno`:
+--
+-- @
+-- tryErrno (throwErrnoOf _ e) ≡ pure (Left e)
+-- @
+throwErrnoOf
+  :: String -- ^ textual description of the error location
+  -> Errno
+  -> IO a
+throwErrnoOf loc errno = ioError (errnoToIOError loc errno Nothing Nothing)
+  where
+  _dummyToSuppressWarnings = error "dummy" getErrno throwErrno
+
 -- | Catches an exception constructed with `errnoToIOError` and extracts `Errno` from it.
 tryErrno :: IO a -> IO (Either Errno a)
 tryErrno = tryJust ioErrorToErrno
@@ -51,6 +73,14 @@
 tryErrno_ :: IO a -> IO Errno
 tryErrno_ = fmap (either id (const eOK)) . tryErrno
 
+-- | Like `tryErrno` but also catches non-Errno errors to return `eIO`.
+tryErrno' :: IO a -> IO (Either Errno a)
+tryErrno' = fmap (either (const $ Left eIO) id) . try_ . tryErrno
+
+-- | Like `tryErrno_` but also catches non-Errno errors to return `eIO`.
+tryErrno_' :: IO a -> IO Errno
+tryErrno_' = fmap (either (const eIO) id) . try_ . tryErrno_
+
 -- | Converts a `TimeSpec` to a `POSIXTime`.
 --
 -- This is the same conversion as the @unix@ package does (as of writing).
@@ -80,3 +110,30 @@
 -- @
 testBitSet :: Bits a => a -> a -> Bool
 testBitSet bits mask = bits .&. mask == mask
+
+-- | Reads from a file descriptor at a given offset.
+--
+-- Fewer bytes may be read than requested.
+-- On error, throws an t`IOError` corresponding to the errno.
+pread :: Fd -> ByteCount -> FileOffset -> IO ByteString
+pread (Fd fd) size off =
+  allocaBytes (fromIntegral size) $ \buf -> do
+    readBytes <- throwErrnoIfMinus1 "pread" $ c_pread fd buf size off
+    B.packCStringLen (buf, fromIntegral readBytes)
+
+-- | Writes to a file descriptor at a given offset.
+--
+-- Returns the number of bytes written. Fewer bytes may be written than requested.
+-- On error, throws an t`IOError` corresponding to the errno.
+pwrite :: Fd -> ByteString -> FileOffset -> IO CSsize
+pwrite (Fd fd) bs off =
+  BU.unsafeUseAsCStringLen bs $ \(buf, size) ->
+    throwErrnoIfMinus1 "pwrite" $ c_pwrite fd buf (fromIntegral size) off
+
+-- | A foreign import of @pread(2)@
+foreign import ccall "pread"
+  c_pread :: CInt -> Ptr a -> CSize -> COff -> IO CSsize
+
+-- | A foreign import of @pwrite(2)@
+foreign import ccall "pwrite"
+  c_pwrite :: CInt -> Ptr a -> CSize -> COff -> IO CSsize
diff --git a/test/unittest/Main.hs b/test/unittest/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/unittest/Main.hs
@@ -0,0 +1,18 @@
+{-# LANGUAGE StandaloneDeriving #-}
+{-# OPTIONS_GHC -Wno-orphans #-}
+module Main where
+
+import Foreign.C (Errno(Errno), eIO)
+import System.LibFuse3
+import Test.Hspec (describe, hspec, it, shouldReturn)
+
+deriving instance Show Errno
+
+main :: IO ()
+main = hspec $ do
+  describe "throwErrnoOf" $ do
+    it "should be an inverse of `tryErrno`" $
+      tryErrno (throwErrnoOf "" eIO) `shouldReturn` (Left eIO :: Either Errno ())
+
+    it "should be an inverse of `tryErrno_`" $
+      tryErrno_ (throwErrnoOf "" eIO) `shouldReturn` eIO
