diff --git a/license.txt b/license.txt
new file mode 100644
--- /dev/null
+++ b/license.txt
@@ -0,0 +1,13 @@
+Copyright 2017 Chris Martin
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/path-text-utf8.cabal b/path-text-utf8.cabal
new file mode 100644
--- /dev/null
+++ b/path-text-utf8.cabal
@@ -0,0 +1,40 @@
+-- This file has been generated from package.yaml by hpack version 0.19.2.
+--
+-- see: https://github.com/sol/hpack
+
+name:           path-text-utf8
+version:        0.0.0.1
+synopsis:       Read and write UTF-8 text files
+
+description:    This is a trivial integration of the @path@ and @text@
+                packages, providing convenient functions to read and
+                write UTF-8 text files.
+category:       Filesystem, Text
+homepage:       https://github.com/chris-martin/path-text-utf8#readme
+bug-reports:    https://github.com/chris-martin/path-text-utf8/issues
+author:         Chris Martin <ch.martin@gmail.com>
+maintainer:     Chris Martin <ch.martin@gmail.com>
+license:        Apache-2.0
+license-file:   license.txt
+build-type:     Simple
+cabal-version:  >= 1.10
+
+source-repository head
+  type: git
+  location: https://github.com/chris-martin/path-text-utf8
+
+library
+  hs-source-dirs:
+      src
+  ghc-options: -Wall
+  build-depends:
+      base >=4.8 && <4.10
+    , bytestring >=0.10 && <0.11
+    , path >=0.5 && <0.6
+    , safe-exceptions >=0.1 && <0.2
+    , text >=1.2 && <1.3
+  exposed-modules:
+      Path.Text.UTF8
+  other-modules:
+      Paths_path_text_utf8
+  default-language: Haskell2010
diff --git a/src/Path/Text/UTF8.hs b/src/Path/Text/UTF8.hs
new file mode 100644
--- /dev/null
+++ b/src/Path/Text/UTF8.hs
@@ -0,0 +1,84 @@
+{-# LANGUAGE BangPatterns      #-}
+{-# LANGUAGE NoImplicitPrelude #-}
+
+-- | Read and write UTF-8 text files.
+module Path.Text.UTF8
+  (
+  -- * Reading
+    readFile
+  , tryReadFile
+  , ReadError (..)
+  -- * Writing
+  , writeFile
+  , tryWriteFile
+  , WriteError
+  -- * Re-exports
+  , IOError
+  , UnicodeException (DecodeError)
+  ) where
+
+-- base
+import Data.Bifunctor  (first)
+import Data.Either     (Either (..))
+import Data.Functor    ((<$>))
+import System.IO       (IO)
+import System.IO.Error (IOError)
+
+-- safe-exceptions
+import qualified Control.Exception.Safe as Exception
+
+-- bytestring
+import qualified Data.ByteString as BS
+
+-- text
+import           Data.Text                (Text)
+import qualified Data.Text.Encoding       as TextEncoding
+import           Data.Text.Encoding.Error (UnicodeException (..))
+
+-- path
+import           Path (Path)
+import qualified Path
+
+data ReadError
+  = ReadErrorIO IOError
+  | ReadErrorDecode UnicodeException
+
+type WriteError = IOError
+
+-- | Read the contents of a UTF-8 encoded text file.
+--
+-- May throw 'IOError' or 'UnicodeException'. To handle these errors in 'Either'
+-- instead, use 'tryReadFile'.
+readFile :: Path base Path.File -> IO Text
+readFile path =
+  f <$> BS.readFile (Path.toFilePath path)
+  where
+    f bs = let !text = TextEncoding.decodeUtf8 bs in text
+
+-- | Read the contents of a UTF-8 encoded text file.
+--
+-- Any 'IOError' or 'UnicodeException' that occurs is caught and returned as a
+-- 'ReadError' on the 'Left' side of the 'Either'. To throw these exceptions
+-- instead, use 'readFile'.
+tryReadFile :: Path base Path.File -> IO (Either ReadError Text)
+tryReadFile path =
+  f <$> Exception.tryIO (BS.readFile (Path.toFilePath path))
+  where
+    f (Left e) = Left (ReadErrorIO e)
+    f (Right bs) = first ReadErrorDecode (TextEncoding.decodeUtf8' bs)
+
+-- | Write text to a file in a UTF-8 encoding.
+--
+-- May throw 'IOError'. To handle this error in 'Either' instead, use
+-- 'tryWriteFile'.
+writeFile :: Path base Path.File -> Text -> IO ()
+writeFile path text =
+  BS.writeFile (Path.toFilePath path) (TextEncoding.encodeUtf8 text)
+
+-- | Write text to a file in a UTF-8 encoding.
+--
+-- Any 'IOError' that occurs is caught and returned on the 'Left' side of the
+-- 'Either'.
+tryWriteFile :: Path base Path.File -> Text -> IO (Either WriteError ())
+tryWriteFile path text =
+  Exception.tryIO (writeFile path text)
