diff --git a/rattletrap.cabal b/rattletrap.cabal
--- a/rattletrap.cabal
+++ b/rattletrap.cabal
@@ -1,7 +1,7 @@
 cabal-version: 2.0
 
 name: rattletrap
-version: 11.0.0
+version: 11.0.1
 synopsis: Parse and generate Rocket League replays.
 description: Rattletrap parses and generates Rocket League replays.
 
@@ -29,7 +29,6 @@
     , http-client >= 0.6.4 && < 0.8
     , http-client-tls >= 0.3.5 && < 0.4
     , text >= 1.2.4 && < 1.3
-    , transformers >= 0.5.6 && < 0.6
   default-extensions: NamedFieldPuns
   default-language: Haskell2010
   exposed-modules:
@@ -47,6 +46,23 @@
     Rattletrap.Console.Mode
     Rattletrap.Console.Option
     Rattletrap.Data
+    Rattletrap.Exception.CrcMismatch
+    Rattletrap.Exception.Empty
+    Rattletrap.Exception.Fail
+    Rattletrap.Exception.InvalidComponent
+    Rattletrap.Exception.InvalidJson
+    Rattletrap.Exception.MissingAttributeLimit
+    Rattletrap.Exception.MissingAttributeName
+    Rattletrap.Exception.MissingClassName
+    Rattletrap.Exception.MissingObjectName
+    Rattletrap.Exception.MissingProductName
+    Rattletrap.Exception.NotEnoughInput
+    Rattletrap.Exception.UnknownActor
+    Rattletrap.Exception.UnknownAttribute
+    Rattletrap.Exception.UnknownName
+    Rattletrap.Exception.UnknownProduct
+    Rattletrap.Exception.UnknownProperty
+    Rattletrap.Exception.UnknownSystemId
     Rattletrap.Get
     Rattletrap.Schema
     Rattletrap.Type.Attribute
diff --git a/src/lib/Rattletrap/BitGet.hs b/src/lib/Rattletrap/BitGet.hs
--- a/src/lib/Rattletrap/BitGet.hs
+++ b/src/lib/Rattletrap/BitGet.hs
@@ -1,11 +1,13 @@
 module Rattletrap.BitGet where
 
+import qualified Control.Exception as Exception
 import qualified Control.Monad as Monad
 import qualified Data.Bits as Bits
 import qualified Data.ByteString as ByteString
 import qualified Data.Functor.Identity as Identity
 import qualified Rattletrap.BitString as BitString
 import qualified Rattletrap.ByteGet as ByteGet
+import qualified Rattletrap.Exception.NotEnoughInput as NotEnoughInput
 import qualified Rattletrap.Get as Get
 
 type BitGet = Get.Get BitString.BitString Identity.Identity
@@ -14,7 +16,7 @@
 toByteGet g = do
   s1 <- Get.get
   case Identity.runIdentity . Get.run g $ BitString.fromByteString s1 of
-    Left e -> fail e
+    Left (ls, e) -> Get.labels ls $ ByteGet.throw e
     Right (s2, x) -> do
       Get.put $ BitString.byteString s2
       pure x
@@ -22,7 +24,7 @@
 fromByteGet :: ByteGet.ByteGet a -> Int -> BitGet a
 fromByteGet f n = do
   x <- byteString n
-  either fail pure $ ByteGet.run f x
+  Get.embed f x
 
 bits :: Bits.Bits a => Int -> BitGet a
 bits n = do
@@ -36,10 +38,16 @@
 bool = do
   s1 <- Get.get
   case BitString.pop s1 of
-    Nothing -> fail "BitGet.bool"
+    Nothing -> throw NotEnoughInput.NotEnoughInput
     Just (x, s2) -> do
       Get.put s2
       pure x
 
 byteString :: Int -> BitGet ByteString.ByteString
 byteString n = fmap ByteString.pack . Monad.replicateM n $ bits 8
+
+throw :: Exception.Exception e => e -> BitGet a
+throw = Get.throw
+
+label :: String -> BitGet a -> BitGet a
+label = Get.label
diff --git a/src/lib/Rattletrap/ByteGet.hs b/src/lib/Rattletrap/ByteGet.hs
--- a/src/lib/Rattletrap/ByteGet.hs
+++ b/src/lib/Rattletrap/ByteGet.hs
@@ -1,5 +1,6 @@
 module Rattletrap.ByteGet where
 
+import qualified Control.Exception as Exception
 import qualified Data.Bits as Bits
 import qualified Data.ByteString as ByteString
 import qualified Data.ByteString.Lazy as LazyByteString
@@ -7,19 +8,26 @@
 import qualified Data.Int as Int
 import qualified Data.Word as Word
 import qualified GHC.Float as Float
+import qualified Rattletrap.Exception.NotEnoughInput as NotEnoughInput
 import qualified Rattletrap.Get as Get
 
 type ByteGet = Get.Get ByteString.ByteString Identity.Identity
 
-run :: ByteGet a -> ByteString.ByteString -> Either String a
+run
+  :: ByteGet a
+  -> ByteString.ByteString
+  -> Either ([String], Exception.SomeException) a
 run g = fmap snd . Identity.runIdentity . Get.run g
 
 byteString :: Int -> ByteGet ByteString.ByteString
 byteString n = do
   s1 <- Get.get
   let (x, s2) = ByteString.splitAt n s1
-  Get.put s2
-  pure x
+  if ByteString.length x == n
+    then do
+      Get.put s2
+      pure x
+    else throw NotEnoughInput.NotEnoughInput
 
 float :: ByteGet Float
 float = fmap Float.castWord32ToFloat word32
@@ -63,3 +71,12 @@
     + Bits.shiftL (fromIntegral $ ByteString.index x 5) 40
     + Bits.shiftL (fromIntegral $ ByteString.index x 6) 48
     + Bits.shiftL (fromIntegral $ ByteString.index x 7) 56
+
+throw :: Exception.Exception e => e -> ByteGet a
+throw = Get.throw
+
+embed :: ByteGet a -> ByteString.ByteString -> ByteGet a
+embed = Get.embed
+
+label :: String -> ByteGet a -> ByteGet a
+label = Get.label
diff --git a/src/lib/Rattletrap/Console/Main.hs b/src/lib/Rattletrap/Console/Main.hs
--- a/src/lib/Rattletrap/Console/Main.hs
+++ b/src/lib/Rattletrap/Console/Main.hs
@@ -1,9 +1,11 @@
 module Rattletrap.Console.Main where
 
+import qualified Control.Exception as Exception
 import qualified Control.Monad as Monad
 import qualified Data.Bool as Bool
 import qualified Data.ByteString as ByteString
 import qualified Data.ByteString.Lazy as LazyByteString
+import qualified Data.List as List
 import qualified Data.Text as Text
 import qualified Network.HTTP.Client as Client
 import qualified Network.HTTP.Client.TLS as Client
@@ -135,7 +137,16 @@
 defaultMain config = do
   input <- getInput config
   let decode = getDecoder config
-  replay <- either fail pure (decode input)
+  replay <- case decode input of
+    Left (ls, e) -> do
+      IO.hPutStr IO.stderr $ unlines
+        [ "ERROR: " <> Exception.displayException e
+        , "# Context: " <> List.intercalate " -> " ls
+        , "# You are using Rattletrap version " <> Version.string
+        , "# Please report this problem at https://github.com/tfausak/rattletrap/issues/new"
+        ]
+      Exit.exitFailure
+    Right x -> pure x
   let encode = getEncoder config
   putOutput config (encode replay)
 
@@ -243,7 +254,9 @@
       ]
 
 getDecoder
-  :: Config.Config -> ByteString.ByteString -> Either String Replay.Replay
+  :: Config.Config
+  -> ByteString.ByteString
+  -> Either ([String], Exception.SomeException) Replay.Replay
 getDecoder config = case Config.getMode config of
   Mode.Decode ->
     Rattletrap.decodeReplayFile (Config.fast config) (Config.skipCrc config)
diff --git a/src/lib/Rattletrap/Data.hs b/src/lib/Rattletrap/Data.hs
--- a/src/lib/Rattletrap/Data.hs
+++ b/src/lib/Rattletrap/Data.hs
@@ -1,12 +1,11 @@
 -- brittany --columns 120
 module Rattletrap.Data where
 
-import qualified Rattletrap.Type.AttributeType as AttributeType
-
 import qualified Data.Bifunctor as Bifunctor
 import qualified Data.Map as Map
 import qualified Data.Set as Set
 import qualified Data.Text as Text
+import qualified Rattletrap.Type.AttributeType as AttributeType
 
 parentClasses :: Map.Map Text.Text Text.Text
 parentClasses = Map.fromList $ fmap
diff --git a/src/lib/Rattletrap/Exception/CrcMismatch.hs b/src/lib/Rattletrap/Exception/CrcMismatch.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/CrcMismatch.hs
@@ -0,0 +1,11 @@
+module Rattletrap.Exception.CrcMismatch where
+
+import qualified Control.Exception as Exception
+import qualified Data.Word as Word
+
+data CrcMismatch = CrcMismatch Word.Word32 Word.Word32
+  deriving (Eq, Show)
+
+instance Exception.Exception CrcMismatch where
+  displayException (CrcMismatch x y) =
+    unwords ["invalid CRC: expected", show x, "but got", show y]
diff --git a/src/lib/Rattletrap/Exception/Empty.hs b/src/lib/Rattletrap/Exception/Empty.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/Empty.hs
@@ -0,0 +1,8 @@
+module Rattletrap.Exception.Empty where
+
+import qualified Control.Exception as Exception
+
+data Empty = Empty
+  deriving (Eq, Show)
+
+instance Exception.Exception Empty
diff --git a/src/lib/Rattletrap/Exception/Fail.hs b/src/lib/Rattletrap/Exception/Fail.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/Fail.hs
@@ -0,0 +1,10 @@
+module Rattletrap.Exception.Fail where
+
+import qualified Control.Exception as Exception
+
+newtype Fail
+  = Fail String
+  deriving (Eq, Show)
+
+instance Exception.Exception Fail where
+  displayException (Fail x) = x
diff --git a/src/lib/Rattletrap/Exception/InvalidComponent.hs b/src/lib/Rattletrap/Exception/InvalidComponent.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/InvalidComponent.hs
@@ -0,0 +1,9 @@
+module Rattletrap.Exception.InvalidComponent where
+
+import qualified Control.Exception as Exception
+
+newtype InvalidComponent
+  = InvalidComponent Word
+  deriving (Eq, Show)
+
+instance Exception.Exception InvalidComponent
diff --git a/src/lib/Rattletrap/Exception/InvalidJson.hs b/src/lib/Rattletrap/Exception/InvalidJson.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/InvalidJson.hs
@@ -0,0 +1,9 @@
+module Rattletrap.Exception.InvalidJson where
+
+import qualified Control.Exception as Exception
+
+newtype InvalidJson
+  = InvalidJson String
+  deriving (Eq, Show)
+
+instance Exception.Exception InvalidJson
diff --git a/src/lib/Rattletrap/Exception/MissingAttributeLimit.hs b/src/lib/Rattletrap/Exception/MissingAttributeLimit.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/MissingAttributeLimit.hs
@@ -0,0 +1,9 @@
+module Rattletrap.Exception.MissingAttributeLimit where
+
+import qualified Control.Exception as Exception
+
+newtype MissingAttributeLimit
+  = MissingAttributeLimit Word
+  deriving (Eq, Show)
+
+instance Exception.Exception MissingAttributeLimit
diff --git a/src/lib/Rattletrap/Exception/MissingAttributeName.hs b/src/lib/Rattletrap/Exception/MissingAttributeName.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/MissingAttributeName.hs
@@ -0,0 +1,9 @@
+module Rattletrap.Exception.MissingAttributeName where
+
+import qualified Control.Exception as Exception
+
+newtype MissingAttributeName
+  = MissingAttributeName Word
+  deriving (Eq, Show)
+
+instance Exception.Exception MissingAttributeName
diff --git a/src/lib/Rattletrap/Exception/MissingClassName.hs b/src/lib/Rattletrap/Exception/MissingClassName.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/MissingClassName.hs
@@ -0,0 +1,9 @@
+module Rattletrap.Exception.MissingClassName where
+
+import qualified Control.Exception as Exception
+
+newtype MissingClassName
+  = MissingClassName String
+  deriving (Eq, Show)
+
+instance Exception.Exception MissingClassName
diff --git a/src/lib/Rattletrap/Exception/MissingObjectName.hs b/src/lib/Rattletrap/Exception/MissingObjectName.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/MissingObjectName.hs
@@ -0,0 +1,10 @@
+module Rattletrap.Exception.MissingObjectName where
+
+import qualified Control.Exception as Exception
+import qualified Data.Word as Word
+
+newtype MissingObjectName
+  = MissingObjectName Word.Word32
+  deriving (Eq, Show)
+
+instance Exception.Exception MissingObjectName
diff --git a/src/lib/Rattletrap/Exception/MissingProductName.hs b/src/lib/Rattletrap/Exception/MissingProductName.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/MissingProductName.hs
@@ -0,0 +1,10 @@
+module Rattletrap.Exception.MissingProductName where
+
+import qualified Control.Exception as Exception
+import qualified Data.Word as Word
+
+newtype MissingProductName
+  = MissingProductName Word.Word32
+  deriving (Eq, Show)
+
+instance Exception.Exception MissingProductName
diff --git a/src/lib/Rattletrap/Exception/NotEnoughInput.hs b/src/lib/Rattletrap/Exception/NotEnoughInput.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/NotEnoughInput.hs
@@ -0,0 +1,8 @@
+module Rattletrap.Exception.NotEnoughInput where
+
+import qualified Control.Exception as Exception
+
+data NotEnoughInput = NotEnoughInput
+  deriving (Eq, Show)
+
+instance Exception.Exception NotEnoughInput
diff --git a/src/lib/Rattletrap/Exception/UnknownActor.hs b/src/lib/Rattletrap/Exception/UnknownActor.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/UnknownActor.hs
@@ -0,0 +1,9 @@
+module Rattletrap.Exception.UnknownActor where
+
+import qualified Control.Exception as Exception
+
+newtype UnknownActor
+  = UnknownActor Word
+  deriving (Eq, Show)
+
+instance Exception.Exception UnknownActor
diff --git a/src/lib/Rattletrap/Exception/UnknownAttribute.hs b/src/lib/Rattletrap/Exception/UnknownAttribute.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/UnknownAttribute.hs
@@ -0,0 +1,9 @@
+module Rattletrap.Exception.UnknownAttribute where
+
+import qualified Control.Exception as Exception
+
+newtype UnknownAttribute
+  = UnknownAttribute String
+  deriving (Eq, Show)
+
+instance Exception.Exception UnknownAttribute
diff --git a/src/lib/Rattletrap/Exception/UnknownName.hs b/src/lib/Rattletrap/Exception/UnknownName.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/UnknownName.hs
@@ -0,0 +1,10 @@
+module Rattletrap.Exception.UnknownName where
+
+import qualified Control.Exception as Exception
+import qualified Data.Word as Word
+
+newtype UnknownName
+  = UnknownName Word.Word32
+  deriving (Eq, Show)
+
+instance Exception.Exception UnknownName
diff --git a/src/lib/Rattletrap/Exception/UnknownProduct.hs b/src/lib/Rattletrap/Exception/UnknownProduct.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/UnknownProduct.hs
@@ -0,0 +1,9 @@
+module Rattletrap.Exception.UnknownProduct where
+
+import qualified Control.Exception as Exception
+
+newtype UnknownProduct
+  = UnknownProduct String
+  deriving (Eq, Show)
+
+instance Exception.Exception UnknownProduct
diff --git a/src/lib/Rattletrap/Exception/UnknownProperty.hs b/src/lib/Rattletrap/Exception/UnknownProperty.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/UnknownProperty.hs
@@ -0,0 +1,9 @@
+module Rattletrap.Exception.UnknownProperty where
+
+import qualified Control.Exception as Exception
+
+newtype UnknownProperty
+  = UnknownProperty String
+  deriving (Eq, Show)
+
+instance Exception.Exception UnknownProperty
diff --git a/src/lib/Rattletrap/Exception/UnknownSystemId.hs b/src/lib/Rattletrap/Exception/UnknownSystemId.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Rattletrap/Exception/UnknownSystemId.hs
@@ -0,0 +1,10 @@
+module Rattletrap.Exception.UnknownSystemId where
+
+import qualified Control.Exception as Exception
+import qualified Data.Word as Word
+
+newtype UnknownSystemId
+  = UnknownSystemId Word.Word8
+  deriving (Eq, Show)
+
+instance Exception.Exception UnknownSystemId
diff --git a/src/lib/Rattletrap/Get.hs b/src/lib/Rattletrap/Get.hs
--- a/src/lib/Rattletrap/Get.hs
+++ b/src/lib/Rattletrap/Get.hs
@@ -1,8 +1,12 @@
 module Rattletrap.Get where
 
 import qualified Control.Applicative as Applicative
+import qualified Control.Exception as Exception
+import qualified Data.Bifunctor as Bifunctor
+import qualified Rattletrap.Exception.Empty as Empty
+import qualified Rattletrap.Exception.Fail as Fail
 
-newtype Get s m a = Get (s -> m (Either String (s, a)))
+newtype Get s m a = Get (s -> m (Either ([String], Exception.SomeException)  (s, a)))
 
 instance Functor m => Functor (Get s m) where
   fmap f g = Get $ fmap (fmap (fmap f)) . run g
@@ -24,10 +28,10 @@
       Right (s2, x) -> run (f x) s2
 
 instance Monad m => MonadFail (Get s m) where
-  fail = Get . const . pure . Left
+  fail = throw . Fail.Fail
 
 instance Monad m => Applicative.Alternative (Get s m) where
-  empty = fail "empty"
+  empty = throw Empty.Empty
 
   gx <|> gy = Get $ \s -> do
     r <- run gx s
@@ -35,7 +39,7 @@
       Left _ -> run gy s
       Right x -> pure $ Right x
 
-run :: Get s m a -> s -> m (Either String (s, a))
+run :: Get s m a -> s -> m (Either ([String], Exception.SomeException) (s, a))
 run (Get f) = f
 
 get :: Applicative m => Get s m s
@@ -46,3 +50,19 @@
 
 lift :: Functor m => m a -> Get s m a
 lift m = Get $ \s -> fmap (\x -> Right (s, x)) m
+
+throw :: (Exception.Exception e, Applicative m) => e -> Get s m a
+throw = Get . const . pure . Left . (,) [] . Exception.toException
+
+embed :: Monad m => Get s m a -> s -> Get t m a
+embed g s = do
+  r <- lift $ run g s
+  case r of
+    Left (ls, e) -> labels ls $ throw e
+    Right (_, x) -> pure x
+
+labels :: Functor m => [String] -> Get s m a -> Get s m a
+labels ls g = Get $ fmap (Bifunctor.first $ Bifunctor.first (ls <>)) . run g
+
+label :: Functor m => String -> Get s m a -> Get s m a
+label = labels . pure
diff --git a/src/lib/Rattletrap/Type/Attribute.hs b/src/lib/Rattletrap/Type/Attribute.hs
--- a/src/lib/Rattletrap/Type/Attribute.hs
+++ b/src/lib/Rattletrap/Type/Attribute.hs
@@ -1,9 +1,13 @@
 module Rattletrap.Type.Attribute where
 
+import qualified Control.Exception as Exception
 import qualified Data.Map as Map
 import Prelude hiding (id)
 import qualified Rattletrap.BitGet as BitGet
 import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Exception.MissingAttributeLimit as MissingAttributeLimit
+import qualified Rattletrap.Exception.MissingAttributeName as MissingAttributeName
+import qualified Rattletrap.Exception.UnknownActor as UnknownActor
 import qualified Rattletrap.Schema as Schema
 import qualified Rattletrap.Type.AttributeValue as AttributeValue
 import qualified Rattletrap.Type.ClassAttributeMap as ClassAttributeMap
@@ -54,12 +58,12 @@
   -> Map.Map CompressedWord.CompressedWord U32.U32
   -> CompressedWord.CompressedWord
   -> BitGet.BitGet Attribute
-bitGet version classes actors actor = do
+bitGet version classes actors actor = BitGet.label "Attribute" $ do
   attributes <- lookupAttributeMap classes actors actor
   limit <- lookupAttributeIdLimit attributes actor
-  id <- CompressedWord.bitGet limit
+  id <- BitGet.label "id" $ CompressedWord.bitGet limit
   name <- lookupAttributeName classes attributes id
-  value <- AttributeValue.bitGet
+  value <- BitGet.label "value" $ AttributeValue.bitGet
     version
     (ClassAttributeMap.objectMap classes)
     name
@@ -71,7 +75,7 @@
   -> CompressedWord.CompressedWord
   -> BitGet.BitGet (Map.Map U32.U32 U32.U32)
 lookupAttributeMap classes actors actor = fromMaybe
-  ("[RT01] could not get attribute map for " <> show actor)
+  (UnknownActor.UnknownActor $ CompressedWord.value actor)
   (ClassAttributeMap.getAttributeMap classes actors actor)
 
 lookupAttributeIdLimit
@@ -79,7 +83,7 @@
   -> CompressedWord.CompressedWord
   -> BitGet.BitGet Word
 lookupAttributeIdLimit attributes actor = fromMaybe
-  ("[RT02] could not get attribute ID limit for " <> show actor)
+  (MissingAttributeLimit.MissingAttributeLimit $ CompressedWord.value actor)
   (ClassAttributeMap.getAttributeIdLimit attributes)
 
 lookupAttributeName
@@ -88,8 +92,8 @@
   -> CompressedWord.CompressedWord
   -> BitGet.BitGet Str.Str
 lookupAttributeName classes attributes attribute = fromMaybe
-  ("[RT03] could not get attribute name for " <> show attribute)
+  (MissingAttributeName.MissingAttributeName $ CompressedWord.value attribute)
   (ClassAttributeMap.getAttributeName classes attributes attribute)
 
-fromMaybe :: String -> Maybe a -> BitGet.BitGet a
-fromMaybe message = maybe (fail message) pure
+fromMaybe :: Exception.Exception e => e -> Maybe a -> BitGet.BitGet a
+fromMaybe message = maybe (BitGet.throw message) pure
diff --git a/src/lib/Rattletrap/Type/Attribute/AppliedDamage.hs b/src/lib/Rattletrap/Type/Attribute/AppliedDamage.hs
--- a/src/lib/Rattletrap/Type/Attribute/AppliedDamage.hs
+++ b/src/lib/Rattletrap/Type/Attribute/AppliedDamage.hs
@@ -49,9 +49,9 @@
     <> I32.bitPut (unknown4 appliedDamageAttribute)
 
 bitGet :: Version.Version -> BitGet.BitGet AppliedDamage
-bitGet version = do
-  unknown1 <- U8.bitGet
-  location <- Vector.bitGet version
-  unknown3 <- I32.bitGet
-  unknown4 <- I32.bitGet
+bitGet version = BitGet.label "AppliedDamage" $ do
+  unknown1 <- BitGet.label "unknown1" U8.bitGet
+  location <- BitGet.label "location" $ Vector.bitGet version
+  unknown3 <- BitGet.label "unknown3" I32.bitGet
+  unknown4 <- BitGet.label "unknown4" I32.bitGet
   pure AppliedDamage { unknown1, location, unknown3, unknown4 }
diff --git a/src/lib/Rattletrap/Type/Attribute/Boolean.hs b/src/lib/Rattletrap/Type/Attribute/Boolean.hs
--- a/src/lib/Rattletrap/Type/Attribute/Boolean.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Boolean.hs
@@ -22,6 +22,6 @@
 bitPut booleanAttribute = BitPut.bool (value booleanAttribute)
 
 bitGet :: BitGet.BitGet Boolean
-bitGet = do
-  value <- BitGet.bool
+bitGet = BitGet.label "Boolean" $ do
+  value <- BitGet.label "value" BitGet.bool
   pure Boolean { value }
diff --git a/src/lib/Rattletrap/Type/Attribute/Byte.hs b/src/lib/Rattletrap/Type/Attribute/Byte.hs
--- a/src/lib/Rattletrap/Type/Attribute/Byte.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Byte.hs
@@ -23,6 +23,6 @@
 bitPut byteAttribute = U8.bitPut (value byteAttribute)
 
 bitGet :: BitGet.BitGet Byte
-bitGet = do
-  value <- U8.bitGet
+bitGet = BitGet.label "Byte" $ do
+  value <- BitGet.label "value" U8.bitGet
   pure Byte { value }
diff --git a/src/lib/Rattletrap/Type/Attribute/CamSettings.hs b/src/lib/Rattletrap/Type/Attribute/CamSettings.hs
--- a/src/lib/Rattletrap/Type/Attribute/CamSettings.hs
+++ b/src/lib/Rattletrap/Type/Attribute/CamSettings.hs
@@ -73,16 +73,15 @@
     <> foldMap F32.bitPut (transitionSpeed camSettingsAttribute)
 
 bitGet :: Version.Version -> BitGet.BitGet CamSettings
-bitGet version = do
-  fov <- F32.bitGet
-  height <- F32.bitGet
-  angle <- F32.bitGet
-  distance <- F32.bitGet
-  stiffness <- F32.bitGet
-  swivelSpeed <- F32.bitGet
-  transitionSpeed <- Monad.whenMaybe
-    (Version.atLeast 868 20 0 version)
-    F32.bitGet
+bitGet version = BitGet.label "CamSettings" $ do
+  fov <- BitGet.label "fov" F32.bitGet
+  height <- BitGet.label "height" F32.bitGet
+  angle <- BitGet.label "angle" F32.bitGet
+  distance <- BitGet.label "distance" F32.bitGet
+  stiffness <- BitGet.label "stiffness" F32.bitGet
+  swivelSpeed <- BitGet.label "swivelSpeed" F32.bitGet
+  transitionSpeed <- BitGet.label "transitionSpeed"
+    $ Monad.whenMaybe (Version.atLeast 868 20 0 version) F32.bitGet
   pure CamSettings
     { fov
     , height
diff --git a/src/lib/Rattletrap/Type/Attribute/ClubColors.hs b/src/lib/Rattletrap/Type/Attribute/ClubColors.hs
--- a/src/lib/Rattletrap/Type/Attribute/ClubColors.hs
+++ b/src/lib/Rattletrap/Type/Attribute/ClubColors.hs
@@ -46,9 +46,9 @@
     <> U8.bitPut (orangeColor clubColorsAttribute)
 
 bitGet :: BitGet.BitGet ClubColors
-bitGet = do
-  blueFlag <- BitGet.bool
-  blueColor <- U8.bitGet
-  orangeFlag <- BitGet.bool
-  orangeColor <- U8.bitGet
+bitGet = BitGet.label "ClubColors" $ do
+  blueFlag <- BitGet.label "blueFlag" BitGet.bool
+  blueColor <- BitGet.label "blueColor" U8.bitGet
+  orangeFlag <- BitGet.label "orangeFlag" BitGet.bool
+  orangeColor <- BitGet.label "orangeColor" U8.bitGet
   pure ClubColors { blueFlag, blueColor, orangeFlag, orangeColor }
diff --git a/src/lib/Rattletrap/Type/Attribute/CustomDemolish.hs b/src/lib/Rattletrap/Type/Attribute/CustomDemolish.hs
--- a/src/lib/Rattletrap/Type/Attribute/CustomDemolish.hs
+++ b/src/lib/Rattletrap/Type/Attribute/CustomDemolish.hs
@@ -44,10 +44,10 @@
     <> Demolish.bitPut (demolish x)
 
 bitGet :: Version.Version -> BitGet.BitGet CustomDemolish
-bitGet version = do
-  flag <- BitGet.bool
-  id <- I32.bitGet
-  demolish <- Demolish.bitGet version
+bitGet version = BitGet.label "CustomDemolish" $ do
+  flag <- BitGet.label "flag" BitGet.bool
+  id <- BitGet.label "id" I32.bitGet
+  demolish <- BitGet.label "demolish" $ Demolish.bitGet version
   pure CustomDemolish
     { flag
     , Rattletrap.Type.Attribute.CustomDemolish.id
diff --git a/src/lib/Rattletrap/Type/Attribute/DamageState.hs b/src/lib/Rattletrap/Type/Attribute/DamageState.hs
--- a/src/lib/Rattletrap/Type/Attribute/DamageState.hs
+++ b/src/lib/Rattletrap/Type/Attribute/DamageState.hs
@@ -66,13 +66,13 @@
     <> BitPut.bool (unknown6 damageStateAttribute)
 
 bitGet :: Version.Version -> BitGet.BitGet DamageState
-bitGet version = do
-  unknown1 <- U8.bitGet
-  unknown2 <- BitGet.bool
-  unknown3 <- I32.bitGet
-  unknown4 <- Vector.bitGet version
-  unknown5 <- BitGet.bool
-  unknown6 <- BitGet.bool
+bitGet version = BitGet.label "CustomDemolish" $ do
+  unknown1 <- BitGet.label "unknown1" U8.bitGet
+  unknown2 <- BitGet.label "unknown2" BitGet.bool
+  unknown3 <- BitGet.label "unknown3" I32.bitGet
+  unknown4 <- BitGet.label "unknown4" $ Vector.bitGet version
+  unknown5 <- BitGet.label "unknown5" BitGet.bool
+  unknown6 <- BitGet.label "unknown6" BitGet.bool
   pure DamageState
     { unknown1
     , unknown2
diff --git a/src/lib/Rattletrap/Type/Attribute/Demolish.hs b/src/lib/Rattletrap/Type/Attribute/Demolish.hs
--- a/src/lib/Rattletrap/Type/Attribute/Demolish.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Demolish.hs
@@ -65,13 +65,13 @@
     <> Vector.bitPut (victimVelocity demolishAttribute)
 
 bitGet :: Version.Version -> BitGet.BitGet Demolish
-bitGet version = do
-  attackerFlag <- BitGet.bool
-  attackerActorId <- U32.bitGet
-  victimFlag <- BitGet.bool
-  victimActorId <- U32.bitGet
-  attackerVelocity <- Vector.bitGet version
-  victimVelocity <- Vector.bitGet version
+bitGet version = BitGet.label "Demolish" $ do
+  attackerFlag <- BitGet.label "attackerFlag" BitGet.bool
+  attackerActorId <- BitGet.label "attackerActorId" U32.bitGet
+  victimFlag <- BitGet.label "victimFlag" BitGet.bool
+  victimActorId <- BitGet.label "victimActorId" U32.bitGet
+  attackerVelocity <- BitGet.label "attackerVelocity" $ Vector.bitGet version
+  victimVelocity <- BitGet.label "victimVelocity" $ Vector.bitGet version
   pure Demolish
     { attackerFlag
     , attackerActorId
diff --git a/src/lib/Rattletrap/Type/Attribute/Enum.hs b/src/lib/Rattletrap/Type/Attribute/Enum.hs
--- a/src/lib/Rattletrap/Type/Attribute/Enum.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Enum.hs
@@ -24,6 +24,6 @@
 bitPut enumAttribute = BitPut.bits 11 (value enumAttribute)
 
 bitGet :: BitGet.BitGet Enum
-bitGet = do
-  value <- BitGet.bits 11
+bitGet = BitGet.label "Enum" $ do
+  value <- BitGet.label "value" $ BitGet.bits 11
   pure Enum { value }
diff --git a/src/lib/Rattletrap/Type/Attribute/Explosion.hs b/src/lib/Rattletrap/Type/Attribute/Explosion.hs
--- a/src/lib/Rattletrap/Type/Attribute/Explosion.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Explosion.hs
@@ -43,8 +43,8 @@
     <> Vector.bitPut (location explosionAttribute)
 
 bitGet :: Version.Version -> BitGet.BitGet Explosion
-bitGet version = do
-  flag <- BitGet.bool
-  actorId <- I32.bitGet
-  location <- Vector.bitGet version
+bitGet version = BitGet.label "Explosion" $ do
+  flag <- BitGet.label "flag" BitGet.bool
+  actorId <- BitGet.label "actorId" I32.bitGet
+  location <- BitGet.label "location" $ Vector.bitGet version
   pure Explosion { flag, actorId, location }
diff --git a/src/lib/Rattletrap/Type/Attribute/ExtendedExplosion.hs b/src/lib/Rattletrap/Type/Attribute/ExtendedExplosion.hs
--- a/src/lib/Rattletrap/Type/Attribute/ExtendedExplosion.hs
+++ b/src/lib/Rattletrap/Type/Attribute/ExtendedExplosion.hs
@@ -34,7 +34,7 @@
 bitPut x = Explosion.bitPut (explosion x) <> FlaggedInt.bitPut (unknown x)
 
 bitGet :: Version.Version -> BitGet.BitGet ExtendedExplosion
-bitGet version = do
-  explosion <- Explosion.bitGet version
-  unknown <- FlaggedInt.bitGet
+bitGet version = BitGet.label "ExtendedExplosion" $ do
+  explosion <- BitGet.label "explosion" $ Explosion.bitGet version
+  unknown <- BitGet.label "unknown" FlaggedInt.bitGet
   pure ExtendedExplosion { explosion, unknown }
diff --git a/src/lib/Rattletrap/Type/Attribute/FlaggedByte.hs b/src/lib/Rattletrap/Type/Attribute/FlaggedByte.hs
--- a/src/lib/Rattletrap/Type/Attribute/FlaggedByte.hs
+++ b/src/lib/Rattletrap/Type/Attribute/FlaggedByte.hs
@@ -33,7 +33,7 @@
   <> U8.bitPut (byte flaggedByteAttribute)
 
 bitGet :: BitGet.BitGet FlaggedByte
-bitGet = do
-  flag <- BitGet.bool
-  byte <- U8.bitGet
+bitGet = BitGet.label "FlaggedByte" $ do
+  flag <- BitGet.label "flag" BitGet.bool
+  byte <- BitGet.label "byte" U8.bitGet
   pure FlaggedByte { flag, byte }
diff --git a/src/lib/Rattletrap/Type/Attribute/FlaggedInt.hs b/src/lib/Rattletrap/Type/Attribute/FlaggedInt.hs
--- a/src/lib/Rattletrap/Type/Attribute/FlaggedInt.hs
+++ b/src/lib/Rattletrap/Type/Attribute/FlaggedInt.hs
@@ -32,7 +32,7 @@
   <> I32.bitPut (int flaggedIntAttribute)
 
 bitGet :: BitGet.BitGet FlaggedInt
-bitGet = do
-  flag <- BitGet.bool
-  int <- I32.bitGet
+bitGet = BitGet.label "FlaggedInt" $ do
+  flag <- BitGet.label "flag" BitGet.bool
+  int <- BitGet.label "int" I32.bitGet
   pure FlaggedInt { flag, int }
diff --git a/src/lib/Rattletrap/Type/Attribute/Float.hs b/src/lib/Rattletrap/Type/Attribute/Float.hs
--- a/src/lib/Rattletrap/Type/Attribute/Float.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Float.hs
@@ -24,6 +24,6 @@
 bitPut floatAttribute = F32.bitPut (value floatAttribute)
 
 bitGet :: BitGet.BitGet Float
-bitGet = do
-  value <- F32.bitGet
+bitGet = BitGet.label "Float" $ do
+  value <- BitGet.label "value" F32.bitGet
   pure Float { value }
diff --git a/src/lib/Rattletrap/Type/Attribute/GameMode.hs b/src/lib/Rattletrap/Type/Attribute/GameMode.hs
--- a/src/lib/Rattletrap/Type/Attribute/GameMode.hs
+++ b/src/lib/Rattletrap/Type/Attribute/GameMode.hs
@@ -38,7 +38,7 @@
   BitPut.bits (numBits gameModeAttribute) (word gameModeAttribute)
 
 bitGet :: Version.Version -> BitGet.BitGet GameMode
-bitGet version = do
+bitGet version = BitGet.label "GameMode" $ do
   let numBits = if Version.atLeast 868 12 0 version then 8 else 2 :: Int
-  word <- BitGet.bits numBits
+  word <- BitGet.label "word" $ BitGet.bits numBits
   pure GameMode { numBits, word }
diff --git a/src/lib/Rattletrap/Type/Attribute/Int.hs b/src/lib/Rattletrap/Type/Attribute/Int.hs
--- a/src/lib/Rattletrap/Type/Attribute/Int.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Int.hs
@@ -24,6 +24,6 @@
 bitPut intAttribute = I32.bitPut (value intAttribute)
 
 bitGet :: BitGet.BitGet Int
-bitGet = do
-  value <- I32.bitGet
+bitGet = BitGet.label "Int" $ do
+  value <- BitGet.label "value" I32.bitGet
   pure Int { value }
diff --git a/src/lib/Rattletrap/Type/Attribute/Int64.hs b/src/lib/Rattletrap/Type/Attribute/Int64.hs
--- a/src/lib/Rattletrap/Type/Attribute/Int64.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Int64.hs
@@ -23,6 +23,6 @@
 putInt64Attribute int64Attribute = I64.bitPut (value int64Attribute)
 
 bitGet :: BitGet.BitGet Int64
-bitGet = do
-  value <- I64.bitGet
+bitGet = BitGet.label "Int64" $ do
+  value <- BitGet.label "value" I64.bitGet
   pure Int64 { value }
diff --git a/src/lib/Rattletrap/Type/Attribute/Loadout.hs b/src/lib/Rattletrap/Type/Attribute/Loadout.hs
--- a/src/lib/Rattletrap/Type/Attribute/Loadout.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Loadout.hs
@@ -132,24 +132,33 @@
     <> foldMap U32.bitPut (unknown6 loadoutAttribute)
 
 bitGet :: BitGet.BitGet Loadout
-bitGet = do
-  version <- U8.bitGet
-  body <- U32.bitGet
-  decal <- U32.bitGet
-  wheels <- U32.bitGet
-  rocketTrail <- U32.bitGet
-  antenna <- U32.bitGet
-  topper <- U32.bitGet
-  unknown1 <- U32.bitGet
-  unknown2 <- Monad.whenMaybe (U8.toWord8 version >= 11) U32.bitGet
-  engineAudio <- Monad.whenMaybe (U8.toWord8 version >= 16) U32.bitGet
-  trail <- Monad.whenMaybe (U8.toWord8 version >= 16) U32.bitGet
-  goalExplosion <- Monad.whenMaybe (U8.toWord8 version >= 16) U32.bitGet
-  banner <- Monad.whenMaybe (U8.toWord8 version >= 17) U32.bitGet
-  unknown3 <- Monad.whenMaybe (U8.toWord8 version >= 19) U32.bitGet
-  unknown4 <- Monad.whenMaybe (U8.toWord8 version >= 22) U32.bitGet
-  unknown5 <- Monad.whenMaybe (U8.toWord8 version >= 22) U32.bitGet
-  unknown6 <- Monad.whenMaybe (U8.toWord8 version >= 22) U32.bitGet
+bitGet = BitGet.label "Loadout" $ do
+  version <- BitGet.label "version" U8.bitGet
+  body <- BitGet.label "body" U32.bitGet
+  decal <- BitGet.label "decal" U32.bitGet
+  wheels <- BitGet.label "wheels" U32.bitGet
+  rocketTrail <- BitGet.label "rocketTrail" U32.bitGet
+  antenna <- BitGet.label "antenna" U32.bitGet
+  topper <- BitGet.label "topper" U32.bitGet
+  unknown1 <- BitGet.label "unknown1" U32.bitGet
+  unknown2 <- BitGet.label "unknown2"
+    $ Monad.whenMaybe (U8.toWord8 version >= 11) U32.bitGet
+  engineAudio <- BitGet.label "engineAudio"
+    $ Monad.whenMaybe (U8.toWord8 version >= 16) U32.bitGet
+  trail <- BitGet.label "trail"
+    $ Monad.whenMaybe (U8.toWord8 version >= 16) U32.bitGet
+  goalExplosion <- BitGet.label "goalExplosion"
+    $ Monad.whenMaybe (U8.toWord8 version >= 16) U32.bitGet
+  banner <- BitGet.label "banner"
+    $ Monad.whenMaybe (U8.toWord8 version >= 17) U32.bitGet
+  unknown3 <- BitGet.label "unknown3"
+    $ Monad.whenMaybe (U8.toWord8 version >= 19) U32.bitGet
+  unknown4 <- BitGet.label "unknown4"
+    $ Monad.whenMaybe (U8.toWord8 version >= 22) U32.bitGet
+  unknown5 <- BitGet.label "unknown5"
+    $ Monad.whenMaybe (U8.toWord8 version >= 22) U32.bitGet
+  unknown6 <- BitGet.label "unknown6"
+    $ Monad.whenMaybe (U8.toWord8 version >= 22) U32.bitGet
   pure Loadout
     { version
     , body
diff --git a/src/lib/Rattletrap/Type/Attribute/LoadoutOnline.hs b/src/lib/Rattletrap/Type/Attribute/LoadoutOnline.hs
--- a/src/lib/Rattletrap/Type/Attribute/LoadoutOnline.hs
+++ b/src/lib/Rattletrap/Type/Attribute/LoadoutOnline.hs
@@ -1,5 +1,6 @@
 module Rattletrap.Type.Attribute.LoadoutOnline where
 
+import qualified Data.Map as Map
 import qualified Rattletrap.BitGet as BitGet
 import qualified Rattletrap.BitPut as BitPut
 import qualified Rattletrap.Schema as Schema
@@ -11,8 +12,6 @@
 import qualified Rattletrap.Type.Version as Version
 import qualified Rattletrap.Utility.Json as Json
 
-import qualified Data.Map as Map
-
 newtype LoadoutOnline = LoadoutOnline
   { value :: List.List (List.List Product.Product)
   } deriving (Eq, Show)
@@ -39,9 +38,10 @@
 
 bitGet
   :: Version.Version -> Map.Map U32.U32 Str.Str -> BitGet.BitGet LoadoutOnline
-bitGet version objectMap = do
-  size <- U8.bitGet
+bitGet version objectMap = BitGet.label "LoadoutOnline" $ do
+  size <- BitGet.label "size" U8.bitGet
   value <-
-    List.replicateM (fromIntegral $ U8.toWord8 size)
-      $ Product.decodeProductAttributesBits version objectMap
+    BitGet.label "value"
+    . List.replicateM (fromIntegral $ U8.toWord8 size)
+    $ Product.decodeProductAttributesBits version objectMap
   pure LoadoutOnline { value }
diff --git a/src/lib/Rattletrap/Type/Attribute/Loadouts.hs b/src/lib/Rattletrap/Type/Attribute/Loadouts.hs
--- a/src/lib/Rattletrap/Type/Attribute/Loadouts.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Loadouts.hs
@@ -33,7 +33,7 @@
   <> Loadout.bitPut (orange loadoutsAttribute)
 
 bitGet :: BitGet.BitGet Loadouts
-bitGet = do
-  blue <- Loadout.bitGet
-  orange <- Loadout.bitGet
+bitGet = BitGet.label "Loadouts" $ do
+  blue <- BitGet.label "blue" Loadout.bitGet
+  orange <- BitGet.label "orange" Loadout.bitGet
   pure Loadouts { blue, orange }
diff --git a/src/lib/Rattletrap/Type/Attribute/LoadoutsOnline.hs b/src/lib/Rattletrap/Type/Attribute/LoadoutsOnline.hs
--- a/src/lib/Rattletrap/Type/Attribute/LoadoutsOnline.hs
+++ b/src/lib/Rattletrap/Type/Attribute/LoadoutsOnline.hs
@@ -1,5 +1,6 @@
 module Rattletrap.Type.Attribute.LoadoutsOnline where
 
+import qualified Data.Map as Map
 import qualified Rattletrap.BitGet as BitGet
 import qualified Rattletrap.BitPut as BitPut
 import qualified Rattletrap.Schema as Schema
@@ -9,8 +10,6 @@
 import qualified Rattletrap.Type.Version as Version
 import qualified Rattletrap.Utility.Json as Json
 
-import qualified Data.Map as Map
-
 data LoadoutsOnline = LoadoutsOnline
   { blue :: LoadoutOnline.LoadoutOnline
   , orange :: LoadoutOnline.LoadoutOnline
@@ -52,9 +51,9 @@
 
 bitGet
   :: Version.Version -> Map.Map U32.U32 Str.Str -> BitGet.BitGet LoadoutsOnline
-bitGet version objectMap = do
-  blue <- LoadoutOnline.bitGet version objectMap
-  orange <- LoadoutOnline.bitGet version objectMap
-  unknown1 <- BitGet.bool
-  unknown2 <- BitGet.bool
+bitGet version objectMap = BitGet.label "LoadoutsOnline" $ do
+  blue <- BitGet.label "blue" $ LoadoutOnline.bitGet version objectMap
+  orange <- BitGet.label "orange" $ LoadoutOnline.bitGet version objectMap
+  unknown1 <- BitGet.label "unknown1" BitGet.bool
+  unknown2 <- BitGet.label "unknown2" BitGet.bool
   pure LoadoutsOnline { blue, orange, unknown1, unknown2 }
diff --git a/src/lib/Rattletrap/Type/Attribute/Location.hs b/src/lib/Rattletrap/Type/Attribute/Location.hs
--- a/src/lib/Rattletrap/Type/Attribute/Location.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Location.hs
@@ -24,6 +24,6 @@
 bitPut locationAttribute = Vector.bitPut (value locationAttribute)
 
 bitGet :: Version.Version -> BitGet.BitGet Location
-bitGet version = do
-  value <- Vector.bitGet version
+bitGet version = BitGet.label "Location" $ do
+  value <- BitGet.label "value" $ Vector.bitGet version
   pure Location { value }
diff --git a/src/lib/Rattletrap/Type/Attribute/MusicStinger.hs b/src/lib/Rattletrap/Type/Attribute/MusicStinger.hs
--- a/src/lib/Rattletrap/Type/Attribute/MusicStinger.hs
+++ b/src/lib/Rattletrap/Type/Attribute/MusicStinger.hs
@@ -42,8 +42,8 @@
     <> U8.bitPut (trigger musicStingerAttribute)
 
 bitGet :: BitGet.BitGet MusicStinger
-bitGet = do
-  flag <- BitGet.bool
-  cue <- U32.bitGet
-  trigger <- U8.bitGet
+bitGet = BitGet.label "MusicStinger" $ do
+  flag <- BitGet.label "flag" BitGet.bool
+  cue <- BitGet.label "cue" U32.bitGet
+  trigger <- BitGet.label "trigger" U8.bitGet
   pure MusicStinger { flag, cue, trigger }
diff --git a/src/lib/Rattletrap/Type/Attribute/PartyLeader.hs b/src/lib/Rattletrap/Type/Attribute/PartyLeader.hs
--- a/src/lib/Rattletrap/Type/Attribute/PartyLeader.hs
+++ b/src/lib/Rattletrap/Type/Attribute/PartyLeader.hs
@@ -51,12 +51,12 @@
     (localId x)
 
 bitGet :: Version.Version -> BitGet.BitGet PartyLeader
-bitGet version = do
-  systemId <- U8.bitGet
+bitGet version = BitGet.label "PartyLeader" $ do
+  systemId <- BitGet.label "systemId" U8.bitGet
   (remoteId, localId) <- if systemId == U8.fromWord8 0
     then pure (Nothing, Nothing)
     else do
-      remoteId <- RemoteId.bitGet version systemId
-      localId <- U8.bitGet
+      remoteId <- BitGet.label "remoteId" $ RemoteId.bitGet version systemId
+      localId <- BitGet.label "localId" U8.bitGet
       pure (Just remoteId, Just localId)
   pure PartyLeader { systemId, remoteId, localId }
diff --git a/src/lib/Rattletrap/Type/Attribute/Pickup.hs b/src/lib/Rattletrap/Type/Attribute/Pickup.hs
--- a/src/lib/Rattletrap/Type/Attribute/Pickup.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Pickup.hs
@@ -40,8 +40,9 @@
     <> BitPut.bool (pickedUp x)
 
 bitGet :: BitGet.BitGet Pickup
-bitGet = do
-  instigator <- BitGet.bool
-  instigatorId <- Monad.whenMaybe instigator U32.bitGet
-  pickedUp <- BitGet.bool
+bitGet = BitGet.label "Pickup" $ do
+  instigator <- BitGet.label "instigator" BitGet.bool
+  instigatorId <- BitGet.label "instigatorId"
+    $ Monad.whenMaybe instigator U32.bitGet
+  pickedUp <- BitGet.label "pickedUp" BitGet.bool
   pure Pickup { instigatorId, pickedUp }
diff --git a/src/lib/Rattletrap/Type/Attribute/PickupNew.hs b/src/lib/Rattletrap/Type/Attribute/PickupNew.hs
--- a/src/lib/Rattletrap/Type/Attribute/PickupNew.hs
+++ b/src/lib/Rattletrap/Type/Attribute/PickupNew.hs
@@ -41,8 +41,9 @@
     <> U8.bitPut (pickedUp x)
 
 bitGet :: BitGet.BitGet PickupNew
-bitGet = do
-  instigator <- BitGet.bool
-  instigatorId <- Monad.whenMaybe instigator U32.bitGet
-  pickedUp <- U8.bitGet
+bitGet = BitGet.label "PickupNew" $ do
+  instigator <- BitGet.label "instigator" BitGet.bool
+  instigatorId <- BitGet.label "instigatorId"
+    $ Monad.whenMaybe instigator U32.bitGet
+  pickedUp <- BitGet.label "pickedUp" U8.bitGet
   pure PickupNew { instigatorId, pickedUp }
diff --git a/src/lib/Rattletrap/Type/Attribute/PlayerHistoryKey.hs b/src/lib/Rattletrap/Type/Attribute/PlayerHistoryKey.hs
--- a/src/lib/Rattletrap/Type/Attribute/PlayerHistoryKey.hs
+++ b/src/lib/Rattletrap/Type/Attribute/PlayerHistoryKey.hs
@@ -24,6 +24,6 @@
 bitPut = BitPut.bits 14 . unknown
 
 bitGet :: BitGet.BitGet PlayerHistoryKey
-bitGet = do
-  unknown <- BitGet.bits 14
+bitGet = BitGet.label "PlayerHistoryKey" $ do
+  unknown <- BitGet.label "unknown" $ BitGet.bits 14
   pure PlayerHistoryKey { unknown }
diff --git a/src/lib/Rattletrap/Type/Attribute/PrivateMatchSettings.hs b/src/lib/Rattletrap/Type/Attribute/PrivateMatchSettings.hs
--- a/src/lib/Rattletrap/Type/Attribute/PrivateMatchSettings.hs
+++ b/src/lib/Rattletrap/Type/Attribute/PrivateMatchSettings.hs
@@ -64,13 +64,13 @@
     <> BitPut.bool (flag privateMatchSettingsAttribute)
 
 bitGet :: BitGet.BitGet PrivateMatchSettings
-bitGet = do
-  mutators <- Str.bitGet
-  joinableBy <- U32.bitGet
-  maxPlayers <- U32.bitGet
-  gameName <- Str.bitGet
-  password <- Str.bitGet
-  flag <- BitGet.bool
+bitGet = BitGet.label "PrivateMatchSettings" $ do
+  mutators <- BitGet.label "mutators" Str.bitGet
+  joinableBy <- BitGet.label "joinableBy" U32.bitGet
+  maxPlayers <- BitGet.label "maxPlayers" U32.bitGet
+  gameName <- BitGet.label "gameName" Str.bitGet
+  password <- BitGet.label "password" Str.bitGet
+  flag <- BitGet.label "flag" BitGet.bool
   pure PrivateMatchSettings
     { mutators
     , joinableBy
diff --git a/src/lib/Rattletrap/Type/Attribute/Product.hs b/src/lib/Rattletrap/Type/Attribute/Product.hs
--- a/src/lib/Rattletrap/Type/Attribute/Product.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Product.hs
@@ -1,5 +1,6 @@
 module Rattletrap.Type.Attribute.Product where
 
+import qualified Data.Map as Map
 import qualified Rattletrap.BitGet as BitGet
 import qualified Rattletrap.BitPut as BitPut
 import qualified Rattletrap.Schema as Schema
@@ -11,8 +12,6 @@
 import qualified Rattletrap.Type.Version as Version
 import qualified Rattletrap.Utility.Json as Json
 
-import qualified Data.Map as Map
-
 data Product = Product
   { unknown :: Bool
   , objectId :: U32.U32
@@ -66,9 +65,10 @@
   List.replicateM (fromIntegral $ U8.toWord8 size) $ bitGet version objectMap
 
 bitGet :: Version.Version -> Map.Map U32.U32 Str.Str -> BitGet.BitGet Product
-bitGet version objectMap = do
-  flag <- BitGet.bool
-  objectId_ <- U32.bitGet
-  let maybeObjectName = Map.lookup objectId_ objectMap
-  value_ <- ProductValue.bitGet version objectId_ maybeObjectName
-  pure (Product flag objectId_ maybeObjectName value_)
+bitGet version objectMap = BitGet.label "Product" $ do
+  unknown <- BitGet.label "unknown" BitGet.bool
+  objectId <- BitGet.label "objectId" U32.bitGet
+  let objectName = Map.lookup objectId objectMap
+  value <- BitGet.label "value"
+    $ ProductValue.bitGet version objectId objectName
+  pure Product { unknown, objectId, objectName, value }
diff --git a/src/lib/Rattletrap/Type/Attribute/ProductValue.hs b/src/lib/Rattletrap/Type/Attribute/ProductValue.hs
--- a/src/lib/Rattletrap/Type/Attribute/ProductValue.hs
+++ b/src/lib/Rattletrap/Type/Attribute/ProductValue.hs
@@ -4,6 +4,8 @@
 import qualified Data.Word as Word
 import qualified Rattletrap.BitGet as BitGet
 import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Exception.MissingProductName as MissingProductName
+import qualified Rattletrap.Exception.UnknownProduct as UnknownProduct
 import qualified Rattletrap.Schema as Schema
 import qualified Rattletrap.Type.CompressedWord as CompressedWord
 import qualified Rattletrap.Type.Str as Str
@@ -75,42 +77,42 @@
 bitGet
   :: Version.Version -> U32.U32 -> Maybe Str.Str -> BitGet.BitGet ProductValue
 bitGet version objectId maybeObjectName =
-  case fmap Str.toString maybeObjectName of
+  BitGet.label "ProductValue" $ case fmap Str.toString maybeObjectName of
     Just "TAGame.ProductAttribute_Painted_TA" -> decodePainted version
     Just "TAGame.ProductAttribute_SpecialEdition_TA" -> decodeSpecialEdition
     Just "TAGame.ProductAttribute_TeamEdition_TA" -> decodeTeamEdition version
     Just "TAGame.ProductAttribute_TitleID_TA" -> decodeTitle
     Just "TAGame.ProductAttribute_UserColor_TA" -> decodeColor version
-    Just objectName -> fail
-      ("[RT05] unknown object name "
-      <> show objectName
-      <> " for ID "
-      <> show objectId
-      )
-    Nothing -> fail ("[RT06] missing object name for ID " <> show objectId)
+    Just x -> BitGet.throw $ UnknownProduct.UnknownProduct x
+    Nothing ->
+      BitGet.throw . MissingProductName.MissingProductName $ U32.toWord32
+        objectId
 
 decodeSpecialEdition :: BitGet.BitGet ProductValue
-decodeSpecialEdition = fmap SpecialEdition $ BitGet.bits 31
+decodeSpecialEdition =
+  BitGet.label "SpecialEdition" . fmap SpecialEdition $ BitGet.bits 31
 
 decodePainted :: Version.Version -> BitGet.BitGet ProductValue
-decodePainted version = if hasNewPainted version
+decodePainted version = BitGet.label "Painted" $ if hasNewPainted version
   then fmap PaintedNew $ BitGet.bits 31
   else fmap PaintedOld $ CompressedWord.bitGet 13
 
 decodeTeamEdition :: Version.Version -> BitGet.BitGet ProductValue
-decodeTeamEdition version = if hasNewPainted version
-  then fmap TeamEditionNew $ BitGet.bits 31
-  else fmap TeamEditionOld $ CompressedWord.bitGet 13
+decodeTeamEdition version =
+  BitGet.label "TeamEdition" $ if hasNewPainted version
+    then fmap TeamEditionNew $ BitGet.bits 31
+    else fmap TeamEditionOld $ CompressedWord.bitGet 13
 
 decodeColor :: Version.Version -> BitGet.BitGet ProductValue
-decodeColor version = if Version.atLeast 868 23 8 version
-  then fmap UserColorNew U32.bitGet
-  else do
-    hasValue <- BitGet.bool
-    fmap UserColorOld $ Monad.whenMaybe hasValue (BitGet.bits 31)
+decodeColor version =
+  BitGet.label "UserColor" $ if Version.atLeast 868 23 8 version
+    then fmap UserColorNew U32.bitGet
+    else do
+      hasValue <- BitGet.bool
+      fmap UserColorOld $ Monad.whenMaybe hasValue (BitGet.bits 31)
 
 hasNewPainted :: Version.Version -> Bool
 hasNewPainted = Version.atLeast 868 18 0
 
 decodeTitle :: BitGet.BitGet ProductValue
-decodeTitle = fmap TitleId Str.bitGet
+decodeTitle = BitGet.label "Title" $ fmap TitleId Str.bitGet
diff --git a/src/lib/Rattletrap/Type/Attribute/QWord.hs b/src/lib/Rattletrap/Type/Attribute/QWord.hs
--- a/src/lib/Rattletrap/Type/Attribute/QWord.hs
+++ b/src/lib/Rattletrap/Type/Attribute/QWord.hs
@@ -23,6 +23,6 @@
 bitPut qWordAttribute = U64.bitPut (value qWordAttribute)
 
 bitGet :: BitGet.BitGet QWord
-bitGet = do
-  value <- U64.bitGet
+bitGet = BitGet.label "QWord" $ do
+  value <- BitGet.label "value" U64.bitGet
   pure QWord { value }
diff --git a/src/lib/Rattletrap/Type/Attribute/Reservation.hs b/src/lib/Rattletrap/Type/Attribute/Reservation.hs
--- a/src/lib/Rattletrap/Type/Attribute/Reservation.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Reservation.hs
@@ -62,14 +62,16 @@
     <> foldMap (BitPut.bits 6) (unknown3 reservationAttribute)
 
 bitGet :: Version.Version -> BitGet.BitGet Reservation
-bitGet version = do
-  number <- CompressedWord.bitGet 7
-  uniqueId <- UniqueId.bitGet version
-  name <- Monad.whenMaybe
+bitGet version = BitGet.label "Reservation" $ do
+  number <- BitGet.label "number" $ CompressedWord.bitGet 7
+  uniqueId <- BitGet.label "uniqueId" $ UniqueId.bitGet version
+  name <- BitGet.label "name" $ Monad.whenMaybe
     (UniqueId.systemId uniqueId /= U8.fromWord8 0)
     Str.bitGet
-  unknown1 <- BitGet.bool
-  unknown2 <- BitGet.bool
-  unknown3 <- Monad.whenMaybe (Version.atLeast 868 12 0 version)
+  unknown1 <- BitGet.label "unknown1" BitGet.bool
+  unknown2 <- BitGet.label "unknown2" BitGet.bool
+  unknown3 <-
+    BitGet.label "unknown3"
+    . Monad.whenMaybe (Version.atLeast 868 12 0 version)
     $ BitGet.bits 6
   pure Reservation { number, uniqueId, name, unknown1, unknown2, unknown3 }
diff --git a/src/lib/Rattletrap/Type/Attribute/RigidBodyState.hs b/src/lib/Rattletrap/Type/Attribute/RigidBodyState.hs
--- a/src/lib/Rattletrap/Type/Attribute/RigidBodyState.hs
+++ b/src/lib/Rattletrap/Type/Attribute/RigidBodyState.hs
@@ -64,12 +64,14 @@
     <> foldMap Vector.bitPut (angularVelocity rigidBodyStateAttribute)
 
 bitGet :: Version.Version -> BitGet.BitGet RigidBodyState
-bitGet version = do
-  sleeping <- BitGet.bool
-  location <- Vector.bitGet version
-  rotation <- Rotation.bitGet version
-  linearVelocity <- Monad.whenMaybe (not sleeping) (Vector.bitGet version)
-  angularVelocity <- Monad.whenMaybe (not sleeping) (Vector.bitGet version)
+bitGet version = BitGet.label "RigidBodyState" $ do
+  sleeping <- BitGet.label "sleeping" BitGet.bool
+  location <- BitGet.label "location" $ Vector.bitGet version
+  rotation <- BitGet.label "rotation" $ Rotation.bitGet version
+  linearVelocity <- BitGet.label "linearVelocity"
+    $ Monad.whenMaybe (not sleeping) (Vector.bitGet version)
+  angularVelocity <- BitGet.label "angularVelocity"
+    $ Monad.whenMaybe (not sleeping) (Vector.bitGet version)
   pure RigidBodyState
     { sleeping
     , location
diff --git a/src/lib/Rattletrap/Type/Attribute/StatEvent.hs b/src/lib/Rattletrap/Type/Attribute/StatEvent.hs
--- a/src/lib/Rattletrap/Type/Attribute/StatEvent.hs
+++ b/src/lib/Rattletrap/Type/Attribute/StatEvent.hs
@@ -33,7 +33,7 @@
   <> I32.bitPut (objectId statEventAttribute)
 
 bitGet :: BitGet.BitGet StatEvent
-bitGet = do
-  unknown <- BitGet.bool
-  objectId <- I32.bitGet
+bitGet = BitGet.label "StatEvent" $ do
+  unknown <- BitGet.label "unknown" BitGet.bool
+  objectId <- BitGet.label "objectId" I32.bitGet
   pure StatEvent { unknown, objectId }
diff --git a/src/lib/Rattletrap/Type/Attribute/String.hs b/src/lib/Rattletrap/Type/Attribute/String.hs
--- a/src/lib/Rattletrap/Type/Attribute/String.hs
+++ b/src/lib/Rattletrap/Type/Attribute/String.hs
@@ -24,6 +24,6 @@
 bitPut stringAttribute = Str.bitPut (value stringAttribute)
 
 bitGet :: BitGet.BitGet String
-bitGet = do
-  value <- Str.bitGet
+bitGet = BitGet.label "String" $ do
+  value <- BitGet.label "value" Str.bitGet
   pure String { value }
diff --git a/src/lib/Rattletrap/Type/Attribute/TeamPaint.hs b/src/lib/Rattletrap/Type/Attribute/TeamPaint.hs
--- a/src/lib/Rattletrap/Type/Attribute/TeamPaint.hs
+++ b/src/lib/Rattletrap/Type/Attribute/TeamPaint.hs
@@ -58,12 +58,12 @@
     <> U32.bitPut (accentFinish teamPaintAttribute)
 
 bitGet :: BitGet.BitGet TeamPaint
-bitGet = do
-  team <- U8.bitGet
-  primaryColor <- U8.bitGet
-  accentColor <- U8.bitGet
-  primaryFinish <- U32.bitGet
-  accentFinish <- U32.bitGet
+bitGet = BitGet.label "TeamPaint" $ do
+  team <- BitGet.label "team" U8.bitGet
+  primaryColor <- BitGet.label "primaryColor" U8.bitGet
+  accentColor <- BitGet.label "accentColor" U8.bitGet
+  primaryFinish <- BitGet.label "primaryFinish" U32.bitGet
+  accentFinish <- BitGet.label "accentFinish" U32.bitGet
   pure TeamPaint
     { team
     , primaryColor
diff --git a/src/lib/Rattletrap/Type/Attribute/Title.hs b/src/lib/Rattletrap/Type/Attribute/Title.hs
--- a/src/lib/Rattletrap/Type/Attribute/Title.hs
+++ b/src/lib/Rattletrap/Type/Attribute/Title.hs
@@ -75,15 +75,15 @@
     <> BitPut.bool (unknown8 titleAttribute)
 
 bitGet :: BitGet.BitGet Title
-bitGet = do
-  unknown1 <- BitGet.bool
-  unknown2 <- BitGet.bool
-  unknown3 <- U32.bitGet
-  unknown4 <- U32.bitGet
-  unknown5 <- U32.bitGet
-  unknown6 <- U32.bitGet
-  unknown7 <- U32.bitGet
-  unknown8 <- BitGet.bool
+bitGet = BitGet.label "Title" $ do
+  unknown1 <- BitGet.label "unknown1" BitGet.bool
+  unknown2 <- BitGet.label "unknown2" BitGet.bool
+  unknown3 <- BitGet.label "unknown3" U32.bitGet
+  unknown4 <- BitGet.label "unknown4" U32.bitGet
+  unknown5 <- BitGet.label "unknown5" U32.bitGet
+  unknown6 <- BitGet.label "unknown6" U32.bitGet
+  unknown7 <- BitGet.label "unknown7" U32.bitGet
+  unknown8 <- BitGet.label "unknown8" BitGet.bool
   pure Title
     { unknown1
     , unknown2
diff --git a/src/lib/Rattletrap/Type/Attribute/UniqueId.hs b/src/lib/Rattletrap/Type/Attribute/UniqueId.hs
--- a/src/lib/Rattletrap/Type/Attribute/UniqueId.hs
+++ b/src/lib/Rattletrap/Type/Attribute/UniqueId.hs
@@ -43,8 +43,8 @@
     <> U8.bitPut (localId uniqueIdAttribute)
 
 bitGet :: Version.Version -> BitGet.BitGet UniqueId
-bitGet version = do
-  systemId <- U8.bitGet
-  remoteId <- RemoteId.bitGet version systemId
-  localId <- U8.bitGet
+bitGet version = BitGet.label "UniqueId" $ do
+  systemId <- BitGet.label "systemId" U8.bitGet
+  remoteId <- BitGet.label "remoteId" $ RemoteId.bitGet version systemId
+  localId <- BitGet.label "localId" U8.bitGet
   pure UniqueId { systemId, remoteId, localId }
diff --git a/src/lib/Rattletrap/Type/Attribute/WeldedInfo.hs b/src/lib/Rattletrap/Type/Attribute/WeldedInfo.hs
--- a/src/lib/Rattletrap/Type/Attribute/WeldedInfo.hs
+++ b/src/lib/Rattletrap/Type/Attribute/WeldedInfo.hs
@@ -55,10 +55,10 @@
     <> Int8Vector.bitPut (rotation weldedInfoAttribute)
 
 bitGet :: Version.Version -> BitGet.BitGet WeldedInfo
-bitGet version = do
-  active <- BitGet.bool
-  actorId <- I32.bitGet
-  offset <- Vector.bitGet version
-  mass <- F32.bitGet
-  rotation <- Int8Vector.bitGet
+bitGet version = BitGet.label "WeldedInfo" $ do
+  active <- BitGet.label "active" BitGet.bool
+  actorId <- BitGet.label "actorId" I32.bitGet
+  offset <- BitGet.label "offset" $ Vector.bitGet version
+  mass <- BitGet.label "mass" F32.bitGet
+  rotation <- BitGet.label "rotation" Int8Vector.bitGet
   pure WeldedInfo { active, actorId, offset, mass, rotation }
diff --git a/src/lib/Rattletrap/Type/AttributeMapping.hs b/src/lib/Rattletrap/Type/AttributeMapping.hs
--- a/src/lib/Rattletrap/Type/AttributeMapping.hs
+++ b/src/lib/Rattletrap/Type/AttributeMapping.hs
@@ -32,7 +32,7 @@
 bytePut x = U32.bytePut (objectId x) <> U32.bytePut (streamId x)
 
 byteGet :: ByteGet.ByteGet AttributeMapping
-byteGet = do
-  objectId <- U32.byteGet
-  streamId <- U32.byteGet
+byteGet = ByteGet.label "AttributeMapping" $ do
+  objectId <- ByteGet.label "objectId" U32.byteGet
+  streamId <- ByteGet.label "streamId" U32.byteGet
   pure AttributeMapping { objectId, streamId }
diff --git a/src/lib/Rattletrap/Type/AttributeValue.hs b/src/lib/Rattletrap/Type/AttributeValue.hs
--- a/src/lib/Rattletrap/Type/AttributeValue.hs
+++ b/src/lib/Rattletrap/Type/AttributeValue.hs
@@ -1,8 +1,11 @@
 module Rattletrap.Type.AttributeValue where
 
+import qualified Data.Foldable as Foldable
+import qualified Data.Map as Map
 import qualified Rattletrap.BitGet as BitGet
 import qualified Rattletrap.BitPut as BitPut
 import qualified Rattletrap.Data as Data
+import qualified Rattletrap.Exception.UnknownAttribute as UnknownAttribute
 import qualified Rattletrap.Schema as Schema
 import qualified Rattletrap.Type.Attribute.AppliedDamage as AppliedDamage
 import qualified Rattletrap.Type.Attribute.Boolean as Boolean
@@ -47,9 +50,6 @@
 import qualified Rattletrap.Type.Version as Version
 import qualified Rattletrap.Utility.Json as Json
 
-import qualified Data.Foldable as Foldable
-import qualified Data.Map as Map
-
 data AttributeValue
   = AppliedDamage AppliedDamage.AppliedDamage
   | Boolean Boolean.Boolean
@@ -259,11 +259,11 @@
   -> Map.Map U32.U32 Str.Str
   -> Str.Str
   -> BitGet.BitGet AttributeValue
-bitGet version objectMap name = do
-  constructor <- maybe
-    (fail ("[RT04] don't know how to get attribute value " <> show name))
-    pure
-    (Map.lookup (Str.toText name) Data.attributeTypes)
+bitGet version objectMap name = BitGet.label "AttributeValue" $ do
+  constructor <- case Map.lookup (Str.toText name) Data.attributeTypes of
+    Nothing ->
+      BitGet.throw . UnknownAttribute.UnknownAttribute $ Str.toString name
+    Just x -> pure x
   case constructor of
     AttributeType.AppliedDamage ->
       fmap AppliedDamage $ AppliedDamage.bitGet version
diff --git a/src/lib/Rattletrap/Type/Cache.hs b/src/lib/Rattletrap/Type/Cache.hs
--- a/src/lib/Rattletrap/Type/Cache.hs
+++ b/src/lib/Rattletrap/Type/Cache.hs
@@ -51,9 +51,10 @@
     <> List.bytePut AttributeMapping.bytePut (attributeMappings x)
 
 byteGet :: ByteGet.ByteGet Cache
-byteGet = do
-  classId <- U32.byteGet
-  parentCacheId <- U32.byteGet
-  cacheId <- U32.byteGet
-  attributeMappings <- List.byteGet AttributeMapping.byteGet
+byteGet = ByteGet.label "Cache" $ do
+  classId <- ByteGet.label "classId" U32.byteGet
+  parentCacheId <- ByteGet.label "parentCacheId" U32.byteGet
+  cacheId <- ByteGet.label "cacheId" U32.byteGet
+  attributeMappings <- ByteGet.label "attributeMappings"
+    $ List.byteGet AttributeMapping.byteGet
   pure Cache { classId, parentCacheId, cacheId, attributeMappings }
diff --git a/src/lib/Rattletrap/Type/ClassAttributeMap.hs b/src/lib/Rattletrap/Type/ClassAttributeMap.hs
--- a/src/lib/Rattletrap/Type/ClassAttributeMap.hs
+++ b/src/lib/Rattletrap/Type/ClassAttributeMap.hs
@@ -1,5 +1,12 @@
 module Rattletrap.Type.ClassAttributeMap where
 
+import qualified Data.IntMap as IntMap
+import qualified Data.List as List
+import qualified Data.Map as Map
+import qualified Data.Maybe as Maybe
+import qualified Data.Set as Set
+import qualified Data.Text as Text
+import qualified Data.Tuple as Tuple
 import qualified Rattletrap.Data as Data
 import qualified Rattletrap.Type.AttributeMapping as AttributeMapping
 import qualified Rattletrap.Type.Cache as Cache
@@ -8,14 +15,6 @@
 import qualified Rattletrap.Type.List as List
 import qualified Rattletrap.Type.Str as Str
 import qualified Rattletrap.Type.U32 as U32
-
-import qualified Data.IntMap as IntMap
-import qualified Data.List as List
-import qualified Data.Map as Map
-import qualified Data.Maybe as Maybe
-import qualified Data.Set as Set
-import qualified Data.Text as Text
-import qualified Data.Tuple as Tuple
 
 -- | This data structure holds all the information about classes, objects, and
 -- attributes in the replay. The class hierarchy is not fixed; it is encoded
diff --git a/src/lib/Rattletrap/Type/ClassMapping.hs b/src/lib/Rattletrap/Type/ClassMapping.hs
--- a/src/lib/Rattletrap/Type/ClassMapping.hs
+++ b/src/lib/Rattletrap/Type/ClassMapping.hs
@@ -33,7 +33,7 @@
 bytePut x = Str.bytePut (name x) <> U32.bytePut (streamId x)
 
 byteGet :: ByteGet.ByteGet ClassMapping
-byteGet = do
-  name <- Str.byteGet
-  streamId <- U32.byteGet
+byteGet = ByteGet.label "ClassMapping" $ do
+  name <- ByteGet.label "name" Str.byteGet
+  streamId <- ByteGet.label "streamId" U32.byteGet
   pure ClassMapping { name, streamId }
diff --git a/src/lib/Rattletrap/Type/CompressedWord.hs b/src/lib/Rattletrap/Type/CompressedWord.hs
--- a/src/lib/Rattletrap/Type/CompressedWord.hs
+++ b/src/lib/Rattletrap/Type/CompressedWord.hs
@@ -1,11 +1,10 @@
 module Rattletrap.Type.CompressedWord where
 
+import qualified Data.Bits as Bits
 import qualified Rattletrap.BitGet as BitGet
 import qualified Rattletrap.BitPut as BitPut
 import qualified Rattletrap.Schema as Schema
 import qualified Rattletrap.Utility.Json as Json
-
-import qualified Data.Bits as Bits
 
 -- | Although there's no guarantee that these values will not overflow, it's
 -- exceptionally unlikely. Most 'CompressedWord's are very small.
diff --git a/src/lib/Rattletrap/Type/Content.hs b/src/lib/Rattletrap/Type/Content.hs
--- a/src/lib/Rattletrap/Type/Content.hs
+++ b/src/lib/Rattletrap/Type/Content.hs
@@ -1,5 +1,8 @@
 module Rattletrap.Type.Content where
 
+import qualified Data.ByteString as ByteString
+import qualified Data.ByteString.Lazy as LazyByteString
+import qualified Data.Word as Word
 import qualified Rattletrap.BitGet as BitGet
 import qualified Rattletrap.BitPut as BitPut
 import qualified Rattletrap.ByteGet as ByteGet
@@ -20,11 +23,6 @@
 import qualified Rattletrap.Utility.Bytes as Bytes
 import qualified Rattletrap.Utility.Json as Json
 
-import qualified Control.Monad.Trans.State as State
-import qualified Data.ByteString as ByteString
-import qualified Data.ByteString.Lazy as LazyByteString
-import qualified Data.Word as Word
-
 type Content = ContentWith (List.List Frame.Frame)
 
 -- | Contains low-level game data about a 'Rattletrap.Replay.Replay'.
@@ -191,32 +189,33 @@
   -- ^ The maximum number of channels in the stream, usually from
   -- 'Rattletrap.Header.getMaxChannels'.
   -> ByteGet.ByteGet Content
-byteGet matchType version numFrames maxChannels = do
-  levels <- List.byteGet Str.byteGet
-  keyframes <- List.byteGet Keyframe.byteGet
-  streamSize <- U32.byteGet
-  stream <- ByteGet.byteString . fromIntegral $ U32.toWord32 streamSize
-  messages <- List.byteGet Message.byteGet
-  marks <- List.byteGet Mark.byteGet
-  packages <- List.byteGet Str.byteGet
-  objects <- List.byteGet Str.byteGet
-  names <- List.byteGet Str.byteGet
-  classMappings <- List.byteGet ClassMapping.byteGet
-  caches <- List.byteGet Cache.byteGet
+byteGet matchType version numFrames maxChannels = ByteGet.label "Content" $ do
+  levels <- ByteGet.label "levels" $ List.byteGet Str.byteGet
+  keyframes <- ByteGet.label "keyframes" $ List.byteGet Keyframe.byteGet
+  streamSize <- ByteGet.label "streamSize" U32.byteGet
+  stream <-
+    ByteGet.label "stream" . ByteGet.byteString . fromIntegral $ U32.toWord32
+      streamSize
+  messages <- ByteGet.label "messages" $ List.byteGet Message.byteGet
+  marks <- ByteGet.label "marks" $ List.byteGet Mark.byteGet
+  packages <- ByteGet.label "packages" $ List.byteGet Str.byteGet
+  objects <- ByteGet.label "objects" $ List.byteGet Str.byteGet
+  names <- ByteGet.label "names" $ List.byteGet Str.byteGet
+  classMappings <- ByteGet.label "classMappings"
+    $ List.byteGet ClassMapping.byteGet
+  caches <- ByteGet.label "caches" $ List.byteGet Cache.byteGet
   let
     classAttributeMap =
       ClassAttributeMap.make objects classMappings caches names
-    bitGet = State.evalStateT
-      (Frame.decodeFramesBits
-        matchType
-        version
-        numFrames
-        maxChannels
-        classAttributeMap
-      )
-      mempty
-  frames <- either fail pure $ ByteGet.run (BitGet.toByteGet bitGet) stream
-  unknown <- fmap LazyByteString.unpack ByteGet.remaining
+    getFrames = BitGet.toByteGet $ Frame.decodeFramesBits
+      matchType
+      version
+      numFrames
+      maxChannels
+      classAttributeMap
+  frames <- ByteGet.label "frames" $ ByteGet.embed getFrames stream
+  unknown <- ByteGet.label "unknown"
+    $ fmap LazyByteString.unpack ByteGet.remaining
   pure Content
     { levels
     , keyframes
diff --git a/src/lib/Rattletrap/Type/Dictionary.hs b/src/lib/Rattletrap/Type/Dictionary.hs
--- a/src/lib/Rattletrap/Type/Dictionary.hs
+++ b/src/lib/Rattletrap/Type/Dictionary.hs
@@ -1,5 +1,8 @@
 module Rattletrap.Type.Dictionary where
 
+import qualified Data.Bifunctor as Bifunctor
+import qualified Data.Map as Map
+import qualified Data.Text as Text
 import qualified Rattletrap.ByteGet as ByteGet
 import qualified Rattletrap.BytePut as BytePut
 import qualified Rattletrap.Schema as Schema
@@ -7,10 +10,6 @@
 import qualified Rattletrap.Type.Str as Str
 import qualified Rattletrap.Utility.Json as Json
 
-import qualified Data.Bifunctor as Bifunctor
-import qualified Data.Map as Map
-import qualified Data.Text as Text
-
 data Dictionary a = Dictionary
   { elements :: List.List (Str.Str, a)
   , lastKey :: Str.Str
@@ -71,7 +70,7 @@
     <> Str.bytePut (lastKey x)
 
 byteGet :: ByteGet.ByteGet a -> ByteGet.ByteGet (Dictionary a)
-byteGet = byteGetWith 0 []
+byteGet = ByteGet.label "Dictionary" . byteGetWith 0 []
 
 byteGetWith
   :: Int
@@ -79,14 +78,14 @@
   -> ByteGet.ByteGet a
   -> ByteGet.ByteGet (Dictionary a)
 byteGetWith i xs f = do
-  k <- Str.byteGet
+  k <- ByteGet.label ("key (" <> show i <> ")") Str.byteGet
   if isNone k
     then pure Dictionary
       { elements = List.fromList . reverse $ fmap snd xs
       , lastKey = k
       }
     else do
-      v <- f
+      v <- ByteGet.label ("value (" <> Str.toString k <> ")") f
       byteGetWith (i + 1) ((i, (k, v)) : xs) f
 
 isNone :: Str.Str -> Bool
diff --git a/src/lib/Rattletrap/Type/F32.hs b/src/lib/Rattletrap/Type/F32.hs
--- a/src/lib/Rattletrap/Type/F32.hs
+++ b/src/lib/Rattletrap/Type/F32.hs
@@ -33,7 +33,7 @@
 bitPut = BitPut.fromBytePut . bytePut
 
 byteGet :: ByteGet.ByteGet F32
-byteGet = fmap fromFloat ByteGet.float
+byteGet = ByteGet.label "F32" $ fmap fromFloat ByteGet.float
 
 bitGet :: BitGet.BitGet F32
 bitGet = BitGet.fromByteGet byteGet 4
diff --git a/src/lib/Rattletrap/Type/Frame.hs b/src/lib/Rattletrap/Type/Frame.hs
--- a/src/lib/Rattletrap/Type/Frame.hs
+++ b/src/lib/Rattletrap/Type/Frame.hs
@@ -1,5 +1,6 @@
 module Rattletrap.Type.Frame where
 
+import qualified Data.Map as Map
 import qualified Rattletrap.BitGet as BitGet
 import qualified Rattletrap.BitPut as BitPut
 import qualified Rattletrap.Schema as Schema
@@ -13,10 +14,6 @@
 import qualified Rattletrap.Type.Version as Version
 import qualified Rattletrap.Utility.Json as Json
 
-import qualified Control.Monad.Trans.Class as Trans
-import qualified Control.Monad.Trans.State as State
-import qualified Data.Map as Map
-
 data Frame = Frame
   { time :: F32.F32
   -- ^ Time in seconds since the beginning of the match.
@@ -65,28 +62,65 @@
   -> Int
   -> Word
   -> ClassAttributeMap.ClassAttributeMap
-  -> State.StateT
-       (Map.Map CompressedWord.CompressedWord U32.U32)
-       BitGet.BitGet
-       (List.List Frame)
+  -> BitGet.BitGet (List.List Frame)
 decodeFramesBits matchType version count limit classes =
-  List.replicateM count $ bitGet matchType version limit classes
+  fmap snd $ decodeFramesBitsWith
+    matchType
+    version
+    count
+    limit
+    classes
+    Map.empty
+    0
+    []
 
+decodeFramesBitsWith
+  :: Maybe Str.Str
+  -> Version.Version
+  -> Int
+  -> Word
+  -> ClassAttributeMap.ClassAttributeMap
+  -> Map.Map CompressedWord.CompressedWord U32.U32
+  -> Int
+  -> [Frame]
+  -> BitGet.BitGet
+       ( Map.Map CompressedWord.CompressedWord U32.U32
+       , List.List Frame
+       )
+decodeFramesBitsWith matchType version count limit classes actorMap index frames
+  = if index >= count
+    then pure (actorMap, List.fromList $ reverse frames)
+    else do
+      (newActorMap, frame) <-
+        BitGet.label ("element (" <> show index <> ")")
+          $ bitGet matchType version limit classes actorMap
+      decodeFramesBitsWith
+          matchType
+          version
+          count
+          limit
+          classes
+          newActorMap
+          (index + 1)
+        $ frame
+        : frames
+
 bitGet
   :: Maybe Str.Str
   -> Version.Version
   -> Word
   -> ClassAttributeMap.ClassAttributeMap
-  -> State.StateT
-       (Map.Map CompressedWord.CompressedWord U32.U32)
-       BitGet.BitGet
-       Frame
-bitGet matchType version limit classes = do
-  time <- Trans.lift F32.bitGet
-  delta <- Trans.lift F32.bitGet
-  replications <- Replication.decodeReplicationsBits
-    matchType
-    version
-    limit
-    classes
-  pure Frame { time, delta, replications }
+  -> Map.Map CompressedWord.CompressedWord U32.U32
+  -> BitGet.BitGet
+       (Map.Map CompressedWord.CompressedWord U32.U32, Frame)
+bitGet matchType version limit classes actorMap = BitGet.label "Frame" $ do
+  time <- BitGet.label "time" F32.bitGet
+  delta <- BitGet.label "delta" F32.bitGet
+  (newActorMap, replications) <-
+    BitGet.label "replications" $ Replication.decodeReplicationsBits
+      matchType
+      version
+      limit
+      classes
+      actorMap
+  pure (newActorMap, Frame { time, delta, replications })
diff --git a/src/lib/Rattletrap/Type/Header.hs b/src/lib/Rattletrap/Type/Header.hs
--- a/src/lib/Rattletrap/Type/Header.hs
+++ b/src/lib/Rattletrap/Type/Header.hs
@@ -97,8 +97,9 @@
     (properties x)
 
 byteGet :: ByteGet.ByteGet Header
-byteGet = do
-  version <- Version.byteGet
-  label <- Str.byteGet
-  properties <- Dictionary.byteGet Property.byteGet
+byteGet = ByteGet.label "Header" $ do
+  version <- ByteGet.label "version" Version.byteGet
+  label <- ByteGet.label "label" Str.byteGet
+  properties <- ByteGet.label "properties"
+    $ Dictionary.byteGet Property.byteGet
   pure Header { version, label, properties }
diff --git a/src/lib/Rattletrap/Type/I32.hs b/src/lib/Rattletrap/Type/I32.hs
--- a/src/lib/Rattletrap/Type/I32.hs
+++ b/src/lib/Rattletrap/Type/I32.hs
@@ -38,7 +38,7 @@
 bitPut = BitPut.fromBytePut . bytePut
 
 byteGet :: ByteGet.ByteGet I32
-byteGet = fmap fromInt32 ByteGet.int32
+byteGet = ByteGet.label "I32" $ fmap fromInt32 ByteGet.int32
 
 bitGet :: BitGet.BitGet I32
 bitGet = BitGet.fromByteGet byteGet 4
diff --git a/src/lib/Rattletrap/Type/I64.hs b/src/lib/Rattletrap/Type/I64.hs
--- a/src/lib/Rattletrap/Type/I64.hs
+++ b/src/lib/Rattletrap/Type/I64.hs
@@ -1,14 +1,13 @@
 module Rattletrap.Type.I64 where
 
+import qualified Data.Int as Int
+import qualified Data.Text as Text
 import qualified Rattletrap.BitGet as BitGet
 import qualified Rattletrap.BitPut as BitPut
 import qualified Rattletrap.ByteGet as ByteGet
 import qualified Rattletrap.BytePut as BytePut
 import qualified Rattletrap.Schema as Schema
 import qualified Rattletrap.Utility.Json as Json
-
-import qualified Data.Int as Int
-import qualified Data.Text as Text
 import qualified Text.Read as Read
 
 newtype I64
diff --git a/src/lib/Rattletrap/Type/I8.hs b/src/lib/Rattletrap/Type/I8.hs
--- a/src/lib/Rattletrap/Type/I8.hs
+++ b/src/lib/Rattletrap/Type/I8.hs
@@ -38,7 +38,7 @@
 bitPut = BitPut.fromBytePut . bytePut
 
 byteGet :: ByteGet.ByteGet I8
-byteGet = fmap fromInt8 ByteGet.int8
+byteGet = ByteGet.label "I8" $ fmap fromInt8 ByteGet.int8
 
 bitGet :: BitGet.BitGet I8
 bitGet = BitGet.fromByteGet byteGet 1
diff --git a/src/lib/Rattletrap/Type/Initialization.hs b/src/lib/Rattletrap/Type/Initialization.hs
--- a/src/lib/Rattletrap/Type/Initialization.hs
+++ b/src/lib/Rattletrap/Type/Initialization.hs
@@ -43,7 +43,9 @@
     <> foldMap Int8Vector.bitPut (rotation initialization)
 
 bitGet :: Version.Version -> Bool -> Bool -> BitGet.BitGet Initialization
-bitGet version hasLocation hasRotation = do
-  location <- Monad.whenMaybe hasLocation (Vector.bitGet version)
-  rotation <- Monad.whenMaybe hasRotation Int8Vector.bitGet
+bitGet version hasLocation hasRotation = BitGet.label "Initialization" $ do
+  location <- BitGet.label "location"
+    $ Monad.whenMaybe hasLocation (Vector.bitGet version)
+  rotation <- BitGet.label "rotation"
+    $ Monad.whenMaybe hasRotation Int8Vector.bitGet
   pure Initialization { location, rotation }
diff --git a/src/lib/Rattletrap/Type/Int8Vector.hs b/src/lib/Rattletrap/Type/Int8Vector.hs
--- a/src/lib/Rattletrap/Type/Int8Vector.hs
+++ b/src/lib/Rattletrap/Type/Int8Vector.hs
@@ -44,10 +44,10 @@
   Just field -> BitPut.bool True <> I8.bitPut field
 
 bitGet :: BitGet.BitGet Int8Vector
-bitGet = do
-  x <- decodeFieldBits
-  y <- decodeFieldBits
-  z <- decodeFieldBits
+bitGet = BitGet.label "Int8Vector" $ do
+  x <- BitGet.label "x" decodeFieldBits
+  y <- BitGet.label "y" decodeFieldBits
+  z <- BitGet.label "z" decodeFieldBits
   pure Int8Vector { x, y, z }
 
 decodeFieldBits :: BitGet.BitGet (Maybe I8.I8)
diff --git a/src/lib/Rattletrap/Type/Keyframe.hs b/src/lib/Rattletrap/Type/Keyframe.hs
--- a/src/lib/Rattletrap/Type/Keyframe.hs
+++ b/src/lib/Rattletrap/Type/Keyframe.hs
@@ -43,8 +43,8 @@
   F32.bytePut (time x) <> U32.bytePut (frame x) <> U32.bytePut (position x)
 
 byteGet :: ByteGet.ByteGet Keyframe
-byteGet = do
-  time <- F32.byteGet
-  frame <- U32.byteGet
-  position <- U32.byteGet
+byteGet = ByteGet.label "Keyframe" $ do
+  time <- ByteGet.label "time" F32.byteGet
+  frame <- ByteGet.label "frame" U32.byteGet
+  position <- ByteGet.label "position" U32.byteGet
   pure Keyframe { time, frame, position }
diff --git a/src/lib/Rattletrap/Type/List.hs b/src/lib/Rattletrap/Type/List.hs
--- a/src/lib/Rattletrap/Type/List.hs
+++ b/src/lib/Rattletrap/Type/List.hs
@@ -1,13 +1,12 @@
 module Rattletrap.Type.List where
 
+import qualified Control.Monad as Monad
 import qualified Rattletrap.ByteGet as ByteGet
 import qualified Rattletrap.BytePut as BytePut
 import qualified Rattletrap.Schema as Schema
 import qualified Rattletrap.Type.U32 as U32
 import qualified Rattletrap.Utility.Json as Json
 
-import qualified Control.Monad as Monad
-
 newtype List a
   = List [a]
   deriving (Eq, Show)
@@ -36,9 +35,13 @@
   in (U32.bytePut . U32.fromWord32 . fromIntegral $ length v) <> foldMap f v
 
 byteGet :: ByteGet.ByteGet a -> ByteGet.ByteGet (List a)
-byteGet f = do
-  size <- U32.byteGet
-  replicateM (fromIntegral $ U32.toWord32 size) f
+byteGet f = ByteGet.label "List" $ do
+  size <- ByteGet.label "size" U32.byteGet
+  generateM (fromIntegral $ U32.toWord32 size)
+    $ \i -> ByteGet.label ("element (" <> show i <> ")") f
+
+generateM :: Monad m => Int -> (Int -> m a) -> m (List a)
+generateM n f = fmap fromList $ mapM f [0 .. n - 1]
 
 replicateM :: Monad m => Int -> m a -> m (List a)
 replicateM n = fmap fromList . Monad.replicateM n
diff --git a/src/lib/Rattletrap/Type/Mark.hs b/src/lib/Rattletrap/Type/Mark.hs
--- a/src/lib/Rattletrap/Type/Mark.hs
+++ b/src/lib/Rattletrap/Type/Mark.hs
@@ -35,7 +35,7 @@
 bytePut x = Str.bytePut (value x) <> U32.bytePut (frame x)
 
 byteGet :: ByteGet.ByteGet Mark
-byteGet = do
-  value <- Str.byteGet
-  frame <- U32.byteGet
+byteGet = ByteGet.label "Mark" $ do
+  value <- ByteGet.label "value" Str.byteGet
+  frame <- ByteGet.label "frame" U32.byteGet
   pure Mark { value, frame }
diff --git a/src/lib/Rattletrap/Type/Message.hs b/src/lib/Rattletrap/Type/Message.hs
--- a/src/lib/Rattletrap/Type/Message.hs
+++ b/src/lib/Rattletrap/Type/Message.hs
@@ -43,8 +43,8 @@
   U32.bytePut (frame x) <> Str.bytePut (name x) <> Str.bytePut (value x)
 
 byteGet :: ByteGet.ByteGet Message
-byteGet = do
-  frame <- U32.byteGet
-  name <- Str.byteGet
-  value <- Str.byteGet
+byteGet = ByteGet.label "Message" $ do
+  frame <- ByteGet.label "frame" U32.byteGet
+  name <- ByteGet.label "name" Str.byteGet
+  value <- ByteGet.label "value" Str.byteGet
   pure Message { frame, name, value }
diff --git a/src/lib/Rattletrap/Type/Property.hs b/src/lib/Rattletrap/Type/Property.hs
--- a/src/lib/Rattletrap/Type/Property.hs
+++ b/src/lib/Rattletrap/Type/Property.hs
@@ -44,8 +44,8 @@
     (value x)
 
 byteGet :: ByteGet.ByteGet Property
-byteGet = do
-  kind <- Str.byteGet
-  size <- U64.byteGet
-  value <- PropertyValue.byteGet byteGet kind
+byteGet = ByteGet.label "Property" $ do
+  kind <- ByteGet.label "kind" Str.byteGet
+  size <- ByteGet.label "size" U64.byteGet
+  value <- ByteGet.label "value" $ PropertyValue.byteGet byteGet kind
   pure Property { kind, size, value }
diff --git a/src/lib/Rattletrap/Type/Property/Array.hs b/src/lib/Rattletrap/Type/Property/Array.hs
--- a/src/lib/Rattletrap/Type/Property/Array.hs
+++ b/src/lib/Rattletrap/Type/Property/Array.hs
@@ -32,4 +32,5 @@
 bytePut f = List.bytePut (Dictionary.bytePut f) . toList
 
 byteGet :: ByteGet.ByteGet a -> ByteGet.ByteGet (Array a)
-byteGet f = fmap fromList $ List.byteGet (Dictionary.byteGet f)
+byteGet =
+  ByteGet.label "Array" . fmap fromList . List.byteGet . Dictionary.byteGet
diff --git a/src/lib/Rattletrap/Type/Property/Bool.hs b/src/lib/Rattletrap/Type/Property/Bool.hs
--- a/src/lib/Rattletrap/Type/Property/Bool.hs
+++ b/src/lib/Rattletrap/Type/Property/Bool.hs
@@ -30,4 +30,4 @@
 bytePut = U8.bytePut . toU8
 
 byteGet :: ByteGet.ByteGet Bool
-byteGet = fmap fromU8 U8.byteGet
+byteGet = ByteGet.label "Bool" $ fmap fromU8 U8.byteGet
diff --git a/src/lib/Rattletrap/Type/Property/Byte.hs b/src/lib/Rattletrap/Type/Property/Byte.hs
--- a/src/lib/Rattletrap/Type/Property/Byte.hs
+++ b/src/lib/Rattletrap/Type/Property/Byte.hs
@@ -29,9 +29,9 @@
 bytePut byte = Str.bytePut (key byte) <> foldMap Str.bytePut (value byte)
 
 byteGet :: ByteGet.ByteGet Byte
-byteGet = do
-  key <- Str.byteGet
-  value <- Monad.whenMaybe
+byteGet = ByteGet.label "Byte" $ do
+  key <- ByteGet.label "key" Str.byteGet
+  value <- ByteGet.label "value" $ Monad.whenMaybe
     (Str.toString key /= "OnlinePlatform_Steam")
     Str.byteGet
   pure Byte { key, value }
diff --git a/src/lib/Rattletrap/Type/Property/Float.hs b/src/lib/Rattletrap/Type/Property/Float.hs
--- a/src/lib/Rattletrap/Type/Property/Float.hs
+++ b/src/lib/Rattletrap/Type/Property/Float.hs
@@ -30,4 +30,4 @@
 bytePut = F32.bytePut . toF32
 
 byteGet :: ByteGet.ByteGet Float
-byteGet = fmap fromF32 F32.byteGet
+byteGet = ByteGet.label "Float" $ fmap fromF32 F32.byteGet
diff --git a/src/lib/Rattletrap/Type/Property/Int.hs b/src/lib/Rattletrap/Type/Property/Int.hs
--- a/src/lib/Rattletrap/Type/Property/Int.hs
+++ b/src/lib/Rattletrap/Type/Property/Int.hs
@@ -30,4 +30,4 @@
 bytePut = I32.bytePut . toI32
 
 byteGet :: ByteGet.ByteGet Int
-byteGet = fmap fromI32 I32.byteGet
+byteGet = ByteGet.label "I32" $ fmap fromI32 I32.byteGet
diff --git a/src/lib/Rattletrap/Type/Property/Name.hs b/src/lib/Rattletrap/Type/Property/Name.hs
--- a/src/lib/Rattletrap/Type/Property/Name.hs
+++ b/src/lib/Rattletrap/Type/Property/Name.hs
@@ -29,4 +29,4 @@
 bytePut = Str.bytePut . toStr
 
 byteGet :: ByteGet.ByteGet Name
-byteGet = fmap fromStr Str.byteGet
+byteGet = ByteGet.label "Name" $ fmap fromStr Str.byteGet
diff --git a/src/lib/Rattletrap/Type/Property/QWord.hs b/src/lib/Rattletrap/Type/Property/QWord.hs
--- a/src/lib/Rattletrap/Type/Property/QWord.hs
+++ b/src/lib/Rattletrap/Type/Property/QWord.hs
@@ -29,4 +29,4 @@
 bytePut = U64.bytePut . toU64
 
 byteGet :: ByteGet.ByteGet QWord
-byteGet = fmap fromU64 U64.byteGet
+byteGet = ByteGet.label "QWord" $ fmap fromU64 U64.byteGet
diff --git a/src/lib/Rattletrap/Type/Property/Str.hs b/src/lib/Rattletrap/Type/Property/Str.hs
--- a/src/lib/Rattletrap/Type/Property/Str.hs
+++ b/src/lib/Rattletrap/Type/Property/Str.hs
@@ -29,4 +29,4 @@
 bytePut = Str.bytePut . toStr
 
 byteGet :: ByteGet.ByteGet Str
-byteGet = fmap fromStr Str.byteGet
+byteGet = ByteGet.label "Str" $ fmap fromStr Str.byteGet
diff --git a/src/lib/Rattletrap/Type/PropertyValue.hs b/src/lib/Rattletrap/Type/PropertyValue.hs
--- a/src/lib/Rattletrap/Type/PropertyValue.hs
+++ b/src/lib/Rattletrap/Type/PropertyValue.hs
@@ -3,6 +3,7 @@
 import qualified Data.Foldable as Foldable
 import qualified Rattletrap.ByteGet as ByteGet
 import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Exception.UnknownProperty as UnknownProperty
 import qualified Rattletrap.Schema as Schema
 import qualified Rattletrap.Type.Property.Array as Property.Array
 import qualified Rattletrap.Type.Property.Bool as Property.Bool
@@ -78,13 +79,14 @@
   Str x -> Property.Str.bytePut x
 
 byteGet :: ByteGet.ByteGet a -> Str.Str -> ByteGet.ByteGet (PropertyValue a)
-byteGet getProperty kind = case Str.toString kind of
-  "ArrayProperty" -> fmap Array $ Property.Array.byteGet getProperty
-  "BoolProperty" -> fmap Bool Property.Bool.byteGet
-  "ByteProperty" -> fmap Byte Property.Byte.byteGet
-  "FloatProperty" -> fmap Float Property.Float.byteGet
-  "IntProperty" -> fmap Int Property.Int.byteGet
-  "NameProperty" -> fmap Name Property.Name.byteGet
-  "QWordProperty" -> fmap QWord Property.QWord.byteGet
-  "StrProperty" -> fmap Str Property.Str.byteGet
-  _ -> fail ("[RT07] don't know how to read property value " <> show kind)
+byteGet getProperty kind =
+  ByteGet.label "PropertyValue" $ case Str.toString kind of
+    "ArrayProperty" -> fmap Array $ Property.Array.byteGet getProperty
+    "BoolProperty" -> fmap Bool Property.Bool.byteGet
+    "ByteProperty" -> fmap Byte Property.Byte.byteGet
+    "FloatProperty" -> fmap Float Property.Float.byteGet
+    "IntProperty" -> fmap Int Property.Int.byteGet
+    "NameProperty" -> fmap Name Property.Name.byteGet
+    "QWordProperty" -> fmap QWord Property.QWord.byteGet
+    "StrProperty" -> fmap Str Property.Str.byteGet
+    x -> ByteGet.throw $ UnknownProperty.UnknownProperty x
diff --git a/src/lib/Rattletrap/Type/Quaternion.hs b/src/lib/Rattletrap/Type/Quaternion.hs
--- a/src/lib/Rattletrap/Type/Quaternion.hs
+++ b/src/lib/Rattletrap/Type/Quaternion.hs
@@ -1,15 +1,15 @@
 module Rattletrap.Type.Quaternion where
 
+import qualified Data.List as List
+import qualified Data.Maybe as Maybe
+import qualified Data.Ord as Ord
 import qualified Rattletrap.BitGet as BitGet
 import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Exception.InvalidComponent as InvalidComponent
 import qualified Rattletrap.Schema as Schema
 import qualified Rattletrap.Type.CompressedWord as CompressedWord
 import qualified Rattletrap.Utility.Json as Json
 
-import qualified Data.List as List
-import qualified Data.Maybe as Maybe
-import qualified Data.Ord as Ord
-
 data Quaternion = Quaternion
   { x :: Double
   , y :: Double
@@ -159,7 +159,7 @@
     1 -> pure Y
     2 -> pure Z
     3 -> pure W
-    y_ -> fail ("[RT08] invalid component: " <> show y_)
+    y_ -> BitGet.throw $ InvalidComponent.InvalidComponent y_
 
 decodePart :: BitGet.BitGet Double
 decodePart = fmap decompressPart $ CompressedWord.bitGet maxCompressedValue
diff --git a/src/lib/Rattletrap/Type/RemoteId.hs b/src/lib/Rattletrap/Type/RemoteId.hs
--- a/src/lib/Rattletrap/Type/RemoteId.hs
+++ b/src/lib/Rattletrap/Type/RemoteId.hs
@@ -1,7 +1,9 @@
 module Rattletrap.Type.RemoteId where
 
+import qualified Data.Foldable as Foldable
 import qualified Rattletrap.BitGet as BitGet
 import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Exception.UnknownSystemId as UnknownSystemId
 import qualified Rattletrap.Schema as Schema
 import qualified Rattletrap.Type.RemoteId.Epic as Epic
 import qualified Rattletrap.Type.RemoteId.PlayStation as PlayStation
@@ -14,8 +16,6 @@
 import qualified Rattletrap.Type.Version as Version
 import qualified Rattletrap.Utility.Json as Json
 
-import qualified Data.Foldable as Foldable
-
 data RemoteId
   = PlayStation PlayStation.PlayStation
   | PsyNet PsyNet.PsyNet
@@ -79,4 +79,4 @@
   6 -> fmap Switch Switch.bitGet
   7 -> fmap PsyNet $ PsyNet.bitGet version
   11 -> fmap Epic Epic.bitGet
-  _ -> fail ("[RT09] unknown system id " <> show systemId)
+  x -> BitGet.throw $ UnknownSystemId.UnknownSystemId x
diff --git a/src/lib/Rattletrap/Type/RemoteId/Epic.hs b/src/lib/Rattletrap/Type/RemoteId/Epic.hs
--- a/src/lib/Rattletrap/Type/RemoteId/Epic.hs
+++ b/src/lib/Rattletrap/Type/RemoteId/Epic.hs
@@ -29,4 +29,4 @@
 bitPut = Str.bitPut . toStr
 
 bitGet :: BitGet.BitGet Epic
-bitGet = fmap fromStr Str.bitGet
+bitGet = BitGet.label "Epic" $ fmap fromStr Str.bitGet
diff --git a/src/lib/Rattletrap/Type/RemoteId/PlayStation.hs b/src/lib/Rattletrap/Type/RemoteId/PlayStation.hs
--- a/src/lib/Rattletrap/Type/RemoteId/PlayStation.hs
+++ b/src/lib/Rattletrap/Type/RemoteId/PlayStation.hs
@@ -37,9 +37,9 @@
   in BitPut.byteString nameBytes <> BitPut.byteString codeBytes
 
 bitGet :: Version.Version -> BitGet.BitGet PlayStation
-bitGet version = do
-  name <- getCode
-  code <- getName version
+bitGet version = BitGet.label "PlayStation" $ do
+  name <- BitGet.label "name" getCode
+  code <- BitGet.label "code" $ getName version
   pure PlayStation { name, code }
 
 getCode :: BitGet.BitGet Text.Text
diff --git a/src/lib/Rattletrap/Type/RemoteId/PsyNet.hs b/src/lib/Rattletrap/Type/RemoteId/PsyNet.hs
--- a/src/lib/Rattletrap/Type/RemoteId/PsyNet.hs
+++ b/src/lib/Rattletrap/Type/RemoteId/PsyNet.hs
@@ -40,11 +40,14 @@
     U64.bitPut a <> U64.bitPut b <> U64.bitPut c <> U64.bitPut d
 
 bitGet :: Version.Version -> BitGet.BitGet PsyNet
-bitGet version = fmap fromEither $ if Version.atLeast 868 24 10 version
-  then fmap Left U64.bitGet
-  else fmap Right $ do
-    a <- U64.bitGet
-    b <- U64.bitGet
-    c <- U64.bitGet
-    d <- U64.bitGet
-    pure (a, b, c, d)
+bitGet version =
+  BitGet.label "PsyNet"
+    . fmap fromEither
+    $ if Version.atLeast 868 24 10 version
+        then BitGet.label "new" $ fmap Left U64.bitGet
+        else BitGet.label "old" . fmap Right $ do
+          a <- U64.bitGet
+          b <- U64.bitGet
+          c <- U64.bitGet
+          d <- U64.bitGet
+          pure (a, b, c, d)
diff --git a/src/lib/Rattletrap/Type/RemoteId/Splitscreen.hs b/src/lib/Rattletrap/Type/RemoteId/Splitscreen.hs
--- a/src/lib/Rattletrap/Type/RemoteId/Splitscreen.hs
+++ b/src/lib/Rattletrap/Type/RemoteId/Splitscreen.hs
@@ -29,4 +29,4 @@
 bitPut = BitPut.bits 24 . toWord32
 
 bitGet :: BitGet.BitGet Splitscreen
-bitGet = fmap fromWord32 $ BitGet.bits 24
+bitGet = BitGet.label "Splitscreen" . fmap fromWord32 $ BitGet.bits 24
diff --git a/src/lib/Rattletrap/Type/RemoteId/Steam.hs b/src/lib/Rattletrap/Type/RemoteId/Steam.hs
--- a/src/lib/Rattletrap/Type/RemoteId/Steam.hs
+++ b/src/lib/Rattletrap/Type/RemoteId/Steam.hs
@@ -29,4 +29,4 @@
 bitPut = U64.bitPut . toU64
 
 bitGet :: BitGet.BitGet Steam
-bitGet = fmap fromU64 U64.bitGet
+bitGet = BitGet.label "Steam" $ fmap fromU64 U64.bitGet
diff --git a/src/lib/Rattletrap/Type/RemoteId/Switch.hs b/src/lib/Rattletrap/Type/RemoteId/Switch.hs
--- a/src/lib/Rattletrap/Type/RemoteId/Switch.hs
+++ b/src/lib/Rattletrap/Type/RemoteId/Switch.hs
@@ -32,7 +32,7 @@
   U64.bitPut (a x) <> U64.bitPut (b x) <> U64.bitPut (c x) <> U64.bitPut (d x)
 
 bitGet :: BitGet.BitGet Switch
-bitGet = do
+bitGet = BitGet.label "Switch" $ do
   a <- U64.bitGet
   b <- U64.bitGet
   c <- U64.bitGet
diff --git a/src/lib/Rattletrap/Type/RemoteId/Xbox.hs b/src/lib/Rattletrap/Type/RemoteId/Xbox.hs
--- a/src/lib/Rattletrap/Type/RemoteId/Xbox.hs
+++ b/src/lib/Rattletrap/Type/RemoteId/Xbox.hs
@@ -29,4 +29,4 @@
 bitPut = U64.bitPut . toU64
 
 bitGet :: BitGet.BitGet Xbox
-bitGet = fmap fromU64 U64.bitGet
+bitGet = BitGet.label "Xbox" $ fmap fromU64 U64.bitGet
diff --git a/src/lib/Rattletrap/Type/Replay.hs b/src/lib/Rattletrap/Type/Replay.hs
--- a/src/lib/Rattletrap/Type/Replay.hs
+++ b/src/lib/Rattletrap/Type/Replay.hs
@@ -64,17 +64,21 @@
   <> Section.bytePut Content.bytePut (content x)
 
 byteGet :: Bool -> Bool -> ByteGet.ByteGet Replay
-byteGet fast skip = do
-  hs <- Section.byteGet skip $ ByteGet.byteString . fromIntegral . U32.toWord32
-  h <- either fail pure . ByteGet.run Header.byteGet $ Section.body hs
-  cs <- Section.byteGet skip $ ByteGet.byteString . fromIntegral . U32.toWord32
-  c <- if fast
-    then pure Content.empty
-    else either fail pure . ByteGet.run (getContent h) $ Section.body cs
-  pure Replay
-    { header = hs { Section.body = h }
-    , content = cs { Section.body = c }
-    }
+byteGet fast skip = ByteGet.label "Replay" $ do
+  header <- ByteGet.label "header" $ do
+    section <-
+      Section.byteGet skip $ ByteGet.byteString . fromIntegral . U32.toWord32
+    body <- ByteGet.embed Header.byteGet $ Section.body section
+    pure section { Section.body }
+  content <- ByteGet.label "content" $ do
+    section <-
+      Section.byteGet skip $ ByteGet.byteString . fromIntegral . U32.toWord32
+    body <- if fast
+      then pure Content.empty
+      else ByteGet.embed (getContent $ Section.body header)
+        $ Section.body section
+    pure section { Section.body }
+  pure Replay { header, content }
 
 getContent :: Header.Header -> ByteGet.ByteGet Content.Content
 getContent h = Content.byteGet
diff --git a/src/lib/Rattletrap/Type/Replication.hs b/src/lib/Rattletrap/Type/Replication.hs
--- a/src/lib/Rattletrap/Type/Replication.hs
+++ b/src/lib/Rattletrap/Type/Replication.hs
@@ -1,5 +1,6 @@
 module Rattletrap.Type.Replication where
 
+import qualified Data.Map as Map
 import qualified Rattletrap.BitGet as BitGet
 import qualified Rattletrap.BitPut as BitPut
 import qualified Rattletrap.Schema as Schema
@@ -11,12 +12,7 @@
 import qualified Rattletrap.Type.U32 as U32
 import qualified Rattletrap.Type.Version as Version
 import qualified Rattletrap.Utility.Json as Json
-import qualified Rattletrap.Utility.Monad as Monad
 
-import qualified Control.Monad.Trans.Class as Trans
-import qualified Control.Monad.Trans.State as State
-import qualified Data.Map as Map
-
 data Replication = Replication
   { actorId :: CompressedWord.CompressedWord
   , value :: ReplicationValue.ReplicationValue
@@ -53,24 +49,58 @@
   -> Version.Version
   -> Word
   -> ClassAttributeMap.ClassAttributeMap
-  -> State.StateT
-       (Map.Map CompressedWord.CompressedWord U32.U32)
-       BitGet.BitGet
-       (List.List Replication)
-decodeReplicationsBits matchType version limit classes = List.untilM $ do
-  p <- Trans.lift BitGet.bool
-  Monad.whenMaybe p $ bitGet matchType version limit classes
+  -> Map.Map CompressedWord.CompressedWord U32.U32
+  -> BitGet.BitGet
+       ( Map.Map CompressedWord.CompressedWord U32.U32
+       , List.List Replication
+       )
+decodeReplicationsBits matchType version limit classes actorMap =
+  decodeReplicationsBitsWith matchType version limit classes actorMap 0 []
 
+decodeReplicationsBitsWith
+  :: Maybe Str.Str
+  -> Version.Version
+  -> Word
+  -> ClassAttributeMap.ClassAttributeMap
+  -> Map.Map CompressedWord.CompressedWord U32.U32
+  -> Int
+  -> [Replication]
+  -> BitGet.BitGet
+       ( Map.Map CompressedWord.CompressedWord U32.U32
+       , List.List Replication
+       )
+decodeReplicationsBitsWith matchType version limit classes actorMap index replications
+  = do
+    hasReplication <- BitGet.bool
+    if hasReplication
+      then do
+        (newActorMap, replication) <-
+          BitGet.label ("element (" <> show index <> ")")
+            $ bitGet matchType version limit classes actorMap
+        decodeReplicationsBitsWith
+            matchType
+            version
+            limit
+            classes
+            newActorMap
+            (index + 1)
+          $ replication
+          : replications
+      else pure (actorMap, List.fromList $ reverse replications)
+
 bitGet
   :: Maybe Str.Str
   -> Version.Version
   -> Word
   -> ClassAttributeMap.ClassAttributeMap
-  -> State.StateT
-       (Map.Map CompressedWord.CompressedWord U32.U32)
-       BitGet.BitGet
-       Replication
-bitGet matchType version limit classes = do
-  actor <- Trans.lift (CompressedWord.bitGet limit)
-  fmap (Replication actor)
-    $ ReplicationValue.bitGet matchType version classes actor
+  -> Map.Map CompressedWord.CompressedWord U32.U32
+  -> BitGet.BitGet
+       ( Map.Map CompressedWord.CompressedWord U32.U32
+       , Replication
+       )
+bitGet matchType version limit classes actorMap =
+  BitGet.label "Replication" $ do
+    actorId <- BitGet.label "actorId" $ CompressedWord.bitGet limit
+    (newActorMap, value) <- BitGet.label "value"
+      $ ReplicationValue.bitGet matchType version classes actorId actorMap
+    pure (newActorMap, Replication { actorId, value })
diff --git a/src/lib/Rattletrap/Type/Replication/Destroyed.hs b/src/lib/Rattletrap/Type/Replication/Destroyed.hs
--- a/src/lib/Rattletrap/Type/Replication/Destroyed.hs
+++ b/src/lib/Rattletrap/Type/Replication/Destroyed.hs
@@ -27,4 +27,4 @@
 bitPut _ = mempty
 
 bitGet :: BitGet.BitGet Destroyed
-bitGet = pure Destroyed
+bitGet = BitGet.label "Destroyed" $ pure Destroyed
diff --git a/src/lib/Rattletrap/Type/Replication/Spawned.hs b/src/lib/Rattletrap/Type/Replication/Spawned.hs
--- a/src/lib/Rattletrap/Type/Replication/Spawned.hs
+++ b/src/lib/Rattletrap/Type/Replication/Spawned.hs
@@ -1,7 +1,11 @@
 module Rattletrap.Type.Replication.Spawned where
 
+import qualified Data.Map as Map
 import qualified Rattletrap.BitGet as BitGet
 import qualified Rattletrap.BitPut as BitPut
+import qualified Rattletrap.Exception.MissingClassName as MissingClassName
+import qualified Rattletrap.Exception.MissingObjectName as MissingObjectName
+import qualified Rattletrap.Exception.UnknownName as UnknownName
 import qualified Rattletrap.Schema as Schema
 import qualified Rattletrap.Type.ClassAttributeMap as ClassAttributeMap
 import qualified Rattletrap.Type.CompressedWord as CompressedWord
@@ -12,10 +16,6 @@
 import qualified Rattletrap.Utility.Json as Json
 import qualified Rattletrap.Utility.Monad as Monad
 
-import qualified Control.Monad.Trans.Class as Trans
-import qualified Control.Monad.Trans.State as State
-import qualified Data.Map as Map
-
 data Spawned = Spawned
   { flag :: Bool
   -- ^ Unclear what this is.
@@ -87,36 +87,34 @@
   -> Version.Version
   -> ClassAttributeMap.ClassAttributeMap
   -> CompressedWord.CompressedWord
-  -> State.StateT
-       (Map.Map CompressedWord.CompressedWord U32.U32)
-       BitGet.BitGet
-       Spawned
-bitGet matchType version classAttributeMap actorId = do
-  flag_ <- Trans.lift BitGet.bool
-  nameIndex_ <- Monad.whenMaybe (hasNameIndex matchType version)
-    $ Trans.lift U32.bitGet
-  name_ <- either fail pure (lookupName classAttributeMap nameIndex_)
-  objectId_ <- Trans.lift U32.bitGet
-  State.modify (Map.insert actorId objectId_)
-  objectName_ <- either
-    fail
+  -> Map.Map CompressedWord.CompressedWord U32.U32
+  -> BitGet.BitGet
+       (Map.Map CompressedWord.CompressedWord U32.U32, Spawned)
+bitGet matchType version classAttributeMap actorId actorMap =
+  BitGet.label "Spawned" $ do
+    flag <- BitGet.label "flag" BitGet.bool
+    nameIndex <- BitGet.label "nameIndex"
+      $ Monad.whenMaybe (hasNameIndex matchType version) U32.bitGet
+    name <- lookupName classAttributeMap nameIndex
+    objectId <- BitGet.label "objectId" U32.bitGet
+    objectName <- lookupObjectName classAttributeMap objectId
+    className <- lookupClassName objectName
+    let hasLocation = ClassAttributeMap.classHasLocation className
+    let hasRotation = ClassAttributeMap.classHasRotation className
+    initialization <- BitGet.label "initialization"
+      $ Initialization.bitGet version hasLocation hasRotation
     pure
-    (lookupObjectName classAttributeMap objectId_)
-  className_ <- either fail pure (lookupClassName objectName_)
-  let hasLocation = ClassAttributeMap.classHasLocation className_
-  let hasRotation = ClassAttributeMap.classHasRotation className_
-  initialization_ <- Trans.lift
-    (Initialization.bitGet version hasLocation hasRotation)
-  pure
-    (Spawned
-      flag_
-      nameIndex_
-      name_
-      objectId_
-      objectName_
-      className_
-      initialization_
-    )
+      ( Map.insert actorId objectId actorMap
+      , Spawned
+        { flag
+        , nameIndex
+        , name
+        , objectId
+        , objectName
+        , className
+        , initialization
+        }
+      )
 
 hasNameIndex :: Maybe Str.Str -> Version.Version -> Bool
 hasNameIndex matchType version =
@@ -125,9 +123,9 @@
 lookupName
   :: ClassAttributeMap.ClassAttributeMap
   -> Maybe U32.U32
-  -> Either String (Maybe Str.Str)
+  -> BitGet.BitGet (Maybe Str.Str)
 lookupName classAttributeMap maybeNameIndex = case maybeNameIndex of
-  Nothing -> Right Nothing
+  Nothing -> pure Nothing
   Just nameIndex_ ->
     case
         ClassAttributeMap.getName
@@ -135,11 +133,11 @@
           nameIndex_
       of
         Nothing ->
-          Left ("[RT11] could not get name for index " <> show nameIndex_)
-        Just name_ -> Right (Just name_)
+          BitGet.throw . UnknownName.UnknownName $ U32.toWord32 nameIndex_
+        Just name_ -> pure (Just name_)
 
 lookupObjectName
-  :: ClassAttributeMap.ClassAttributeMap -> U32.U32 -> Either String Str.Str
+  :: ClassAttributeMap.ClassAttributeMap -> U32.U32 -> BitGet.BitGet Str.Str
 lookupObjectName classAttributeMap objectId_ =
   case
       ClassAttributeMap.getObjectName
@@ -147,12 +145,13 @@
         objectId_
     of
       Nothing ->
-        Left ("[RT12] could not get object name for id " <> show objectId_)
-      Just objectName_ -> Right objectName_
+        BitGet.throw . MissingObjectName.MissingObjectName $ U32.toWord32
+          objectId_
+      Just objectName_ -> pure objectName_
 
-lookupClassName :: Str.Str -> Either String Str.Str
+lookupClassName :: Str.Str -> BitGet.BitGet Str.Str
 lookupClassName objectName_ =
   case ClassAttributeMap.getClassName objectName_ of
-    Nothing ->
-      Left ("[RT13] could not get class name for object " <> show objectName_)
-    Just className_ -> Right className_
+    Nothing -> BitGet.throw . MissingClassName.MissingClassName $ Str.toString
+      objectName_
+    Just className_ -> pure className_
diff --git a/src/lib/Rattletrap/Type/Replication/Updated.hs b/src/lib/Rattletrap/Type/Replication/Updated.hs
--- a/src/lib/Rattletrap/Type/Replication/Updated.hs
+++ b/src/lib/Rattletrap/Type/Replication/Updated.hs
@@ -1,5 +1,6 @@
 module Rattletrap.Type.Replication.Updated where
 
+import qualified Data.Map as Map
 import qualified Rattletrap.BitGet as BitGet
 import qualified Rattletrap.BitPut as BitPut
 import qualified Rattletrap.Schema as Schema
@@ -12,8 +13,6 @@
 import qualified Rattletrap.Utility.Json as Json
 import qualified Rattletrap.Utility.Monad as Monad
 
-import qualified Data.Map as Map
-
 newtype Updated = Updated
   { attributes :: List.List Attribute.Attribute
   } deriving (Eq, Show)
@@ -41,6 +40,7 @@
   -> Map.Map CompressedWord.CompressedWord U32.U32
   -> CompressedWord.CompressedWord
   -> BitGet.BitGet Updated
-bitGet version classes actors actor = fmap Updated . List.untilM $ do
-  p <- BitGet.bool
-  Monad.whenMaybe p $ Attribute.bitGet version classes actors actor
+bitGet version classes actors actor =
+  BitGet.label "Updated" . fmap Updated . List.untilM $ do
+    p <- BitGet.bool
+    Monad.whenMaybe p $ Attribute.bitGet version classes actors actor
diff --git a/src/lib/Rattletrap/Type/ReplicationValue.hs b/src/lib/Rattletrap/Type/ReplicationValue.hs
--- a/src/lib/Rattletrap/Type/ReplicationValue.hs
+++ b/src/lib/Rattletrap/Type/ReplicationValue.hs
@@ -1,5 +1,7 @@
 module Rattletrap.Type.ReplicationValue where
 
+import qualified Data.Foldable as Foldable
+import qualified Data.Map as Map
 import qualified Rattletrap.BitGet as BitGet
 import qualified Rattletrap.BitPut as BitPut
 import qualified Rattletrap.Schema as Schema
@@ -13,11 +15,6 @@
 import qualified Rattletrap.Type.Version as Version
 import qualified Rattletrap.Utility.Json as Json
 
-import qualified Control.Monad.Trans.Class as Trans
-import qualified Control.Monad.Trans.State as State
-import qualified Data.Foldable as Foldable
-import qualified Data.Map as Map
-
 data ReplicationValue
   = Spawned Spawned.Spawned
   -- ^ Creates a new actor.
@@ -59,22 +56,33 @@
   -> Version.Version
   -> ClassAttributeMap.ClassAttributeMap
   -> CompressedWord.CompressedWord
-  -> State.StateT
-       (Map.Map CompressedWord.CompressedWord U32.U32)
-       BitGet.BitGet
-       ReplicationValue
-bitGet matchType version classAttributeMap actorId = do
-  actorMap <- State.get
-  isOpen <- Trans.lift BitGet.bool
-  if isOpen
-    then do
-      isNew <- Trans.lift BitGet.bool
-      if isNew
-        then fmap Spawned
-          $ Spawned.bitGet matchType version classAttributeMap actorId
-        else fmap Updated . Trans.lift $ Updated.bitGet
-          version
-          classAttributeMap
-          actorMap
-          actorId
-    else fmap Destroyed $ Trans.lift Destroyed.bitGet
+  -> Map.Map CompressedWord.CompressedWord U32.U32
+  -> BitGet.BitGet
+       ( Map.Map CompressedWord.CompressedWord U32.U32
+       , ReplicationValue
+       )
+bitGet matchType version classAttributeMap actorId actorMap =
+  BitGet.label "ReplicationValue" $ do
+    isOpen <- BitGet.bool
+    if isOpen
+      then do
+        isNew <- BitGet.bool
+        if isNew
+          then do
+            (newActorMap, spawned) <- Spawned.bitGet
+              matchType
+              version
+              classAttributeMap
+              actorId
+              actorMap
+            pure (newActorMap, Spawned spawned)
+          else do
+            updated <- Updated.bitGet
+              version
+              classAttributeMap
+              actorMap
+              actorId
+            pure (actorMap, Updated updated)
+      else do
+        destroyed <- Destroyed.bitGet
+        pure (actorMap, Destroyed destroyed)
diff --git a/src/lib/Rattletrap/Type/Section.hs b/src/lib/Rattletrap/Type/Section.hs
--- a/src/lib/Rattletrap/Type/Section.hs
+++ b/src/lib/Rattletrap/Type/Section.hs
@@ -1,16 +1,16 @@
 module Rattletrap.Type.Section where
 
+import qualified Control.Monad as Monad
+import qualified Data.ByteString as ByteString
+import qualified Data.Text as Text
 import qualified Rattletrap.ByteGet as ByteGet
 import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Exception.CrcMismatch as CrcMismatch
 import qualified Rattletrap.Schema as Schema
 import qualified Rattletrap.Type.U32 as U32
 import qualified Rattletrap.Utility.Crc as Crc
 import qualified Rattletrap.Utility.Json as Json
 
-import qualified Control.Monad as Monad
-import qualified Data.ByteString as ByteString
-import qualified Data.Text as Text
-
 -- | A section is a large piece of a 'Rattletrap.Replay.Replay'. It has a
 -- 32-bit size (in bytes), a 32-bit CRC (see "Rattletrap.Utility.Crc"), and then a
 -- bunch of data (the body). This interface is provided so that you don't have
@@ -72,15 +72,20 @@
 
 byteGet
   :: Bool -> (U32.U32 -> ByteGet.ByteGet a) -> ByteGet.ByteGet (Section a)
-byteGet skip getBody = do
-  size_ <- U32.byteGet
-  crc_ <- U32.byteGet
-  rawBody <- ByteGet.byteString (fromIntegral (U32.toWord32 size_))
-  Monad.unless skip $ do
-    let actualCrc = U32.fromWord32 (Crc.compute rawBody)
-    Monad.when (actualCrc /= crc_) (fail (crcMessage actualCrc crc_))
-  body_ <- either fail pure $ ByteGet.run (getBody size_) rawBody
-  pure (Section size_ crc_ body_)
+byteGet skip getBody = ByteGet.label "Section" $ do
+  size <- ByteGet.label "size" U32.byteGet
+  crc <- ByteGet.label "crc" U32.byteGet
+  body <- ByteGet.label "body" $ do
+    rawBody <- ByteGet.byteString . fromIntegral $ U32.toWord32 size
+    Monad.unless skip $ do
+      let
+        expected = U32.toWord32 crc
+        actual = Crc.compute rawBody
+      Monad.when (actual /= expected) . ByteGet.throw $ CrcMismatch.CrcMismatch
+        expected
+        actual
+    ByteGet.embed (getBody size) rawBody
+  pure Section { size, crc, body }
 
 crcMessage :: U32.U32 -> U32.U32 -> String
 crcMessage actual expected = unwords
diff --git a/src/lib/Rattletrap/Type/Str.hs b/src/lib/Rattletrap/Type/Str.hs
--- a/src/lib/Rattletrap/Type/Str.hs
+++ b/src/lib/Rattletrap/Type/Str.hs
@@ -1,5 +1,11 @@
 module Rattletrap.Type.Str where
 
+import qualified Data.ByteString as ByteString
+import qualified Data.Char as Char
+import qualified Data.Int as Int
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as Text
+import qualified Data.Text.Encoding.Error as Text
 import qualified Rattletrap.BitGet as BitGet
 import qualified Rattletrap.BitPut as BitPut
 import qualified Rattletrap.ByteGet as ByteGet
@@ -9,13 +15,6 @@
 import qualified Rattletrap.Utility.Bytes as Bytes
 import qualified Rattletrap.Utility.Json as Json
 
-import qualified Data.ByteString as ByteString
-import qualified Data.Char as Char
-import qualified Data.Int as Int
-import qualified Data.Text as Text
-import qualified Data.Text.Encoding as Text
-import qualified Data.Text.Encoding.Error as Text
-
 newtype Str
   = Str Text.Text
   deriving (Eq, Ord, Show)
@@ -73,10 +72,10 @@
 addNull text = if Text.null text then text else Text.snoc text '\x00'
 
 byteGet :: ByteGet.ByteGet Str
-byteGet = do
-  rawSize <- I32.byteGet
-  bytes <- ByteGet.byteString (normalizeTextSize rawSize)
-  pure (fromText (dropNull (getTextDecoder rawSize bytes)))
+byteGet = ByteGet.label "Str" $ do
+  size <- ByteGet.label "size" I32.byteGet
+  bytes <- ByteGet.label "value" . ByteGet.byteString $ normalizeTextSize size
+  pure . fromText . dropNull $ getTextDecoder size bytes
 
 bitGet :: BitGet.BitGet Str
 bitGet = do
diff --git a/src/lib/Rattletrap/Type/U32.hs b/src/lib/Rattletrap/Type/U32.hs
--- a/src/lib/Rattletrap/Type/U32.hs
+++ b/src/lib/Rattletrap/Type/U32.hs
@@ -38,7 +38,7 @@
 bitPut = BitPut.fromBytePut . bytePut
 
 byteGet :: ByteGet.ByteGet U32
-byteGet = fmap fromWord32 ByteGet.word32
+byteGet = ByteGet.label "U32" $ fmap fromWord32 ByteGet.word32
 
 bitGet :: BitGet.BitGet U32
 bitGet = BitGet.fromByteGet byteGet 4
diff --git a/src/lib/Rattletrap/Type/U64.hs b/src/lib/Rattletrap/Type/U64.hs
--- a/src/lib/Rattletrap/Type/U64.hs
+++ b/src/lib/Rattletrap/Type/U64.hs
@@ -1,14 +1,13 @@
 module Rattletrap.Type.U64 where
 
+import qualified Data.Text as Text
+import qualified Data.Word as Word
 import qualified Rattletrap.BitGet as BitGet
 import qualified Rattletrap.BitPut as BitPut
 import qualified Rattletrap.ByteGet as ByteGet
 import qualified Rattletrap.BytePut as BytePut
 import qualified Rattletrap.Schema as Schema
 import qualified Rattletrap.Utility.Json as Json
-
-import qualified Data.Text as Text
-import qualified Data.Word as Word
 import qualified Text.Read as Read
 
 newtype U64
@@ -42,7 +41,7 @@
 bitPut = BitPut.fromBytePut . bytePut
 
 byteGet :: ByteGet.ByteGet U64
-byteGet = fmap fromWord64 ByteGet.word64
+byteGet = ByteGet.label "U64" $ fmap fromWord64 ByteGet.word64
 
 bitGet :: BitGet.BitGet U64
 bitGet = BitGet.fromByteGet byteGet 8
diff --git a/src/lib/Rattletrap/Type/U8.hs b/src/lib/Rattletrap/Type/U8.hs
--- a/src/lib/Rattletrap/Type/U8.hs
+++ b/src/lib/Rattletrap/Type/U8.hs
@@ -38,7 +38,7 @@
 bitPut = BitPut.fromBytePut . bytePut
 
 byteGet :: ByteGet.ByteGet U8
-byteGet = fmap fromWord8 ByteGet.word8
+byteGet = ByteGet.label "U8" $ fmap fromWord8 ByteGet.word8
 
 bitGet :: BitGet.BitGet U8
 bitGet = BitGet.fromByteGet byteGet 1
diff --git a/src/lib/Rattletrap/Type/Vector.hs b/src/lib/Rattletrap/Type/Vector.hs
--- a/src/lib/Rattletrap/Type/Vector.hs
+++ b/src/lib/Rattletrap/Type/Vector.hs
@@ -66,15 +66,17 @@
     <> CompressedWord.bitPut (CompressedWord.CompressedWord limit dz)
 
 bitGet :: Version.Version -> BitGet.BitGet Vector
-bitGet version = do
-  size <- CompressedWord.bitGet
+bitGet version = BitGet.label "Vector" $ do
+  size <-
+    BitGet.label "size"
+    . CompressedWord.bitGet
     $ if Version.atLeast 868 22 7 version then 21 else 19
   let
     limit = getLimit size
     bias = getBias size
-  x <- getPart limit bias
-  y <- getPart limit bias
-  z <- getPart limit bias
+  x <- BitGet.label "x" $ getPart limit bias
+  y <- BitGet.label "y" $ getPart limit bias
+  z <- BitGet.label "z" $ getPart limit bias
   pure Vector { size, bias, x, y, z }
 
 getPart :: Word -> Word -> BitGet.BitGet Int
diff --git a/src/lib/Rattletrap/Type/Version.hs b/src/lib/Rattletrap/Type/Version.hs
--- a/src/lib/Rattletrap/Type/Version.hs
+++ b/src/lib/Rattletrap/Type/Version.hs
@@ -24,10 +24,10 @@
   (patch x)
 
 byteGet :: ByteGet.ByteGet Version
-byteGet = do
-  major <- U32.byteGet
-  minor <- U32.byteGet
-  patch <- Monad.whenMaybe
+byteGet = ByteGet.label "Version" $ do
+  major <- ByteGet.label "major" U32.byteGet
+  minor <- ByteGet.label "minor" U32.byteGet
+  patch <- ByteGet.label "patch" $ Monad.whenMaybe
     (U32.toWord32 major >= 868 && U32.toWord32 minor >= 18)
     U32.byteGet
   pure Version { major, minor, patch }
diff --git a/src/lib/Rattletrap/Utility/Helper.hs b/src/lib/Rattletrap/Utility/Helper.hs
--- a/src/lib/Rattletrap/Utility/Helper.hs
+++ b/src/lib/Rattletrap/Utility/Helper.hs
@@ -4,17 +4,23 @@
 
 import qualified Rattletrap.ByteGet as ByteGet
 import qualified Rattletrap.BytePut as BytePut
+import qualified Rattletrap.Exception.InvalidJson as InvalidJson
 import qualified Rattletrap.Type.Content as Content
 import qualified Rattletrap.Type.Replay as Replay
 import qualified Rattletrap.Type.Section as Section
 import qualified Rattletrap.Utility.Json as Json
 
+import qualified Control.Exception as Exception
+import qualified Data.Bifunctor as Bifunctor
 import qualified Data.ByteString as ByteString
 import qualified Data.ByteString.Lazy as LazyByteString
 
 -- | Parses a raw replay.
 decodeReplayFile
-  :: Bool -> Bool -> ByteString.ByteString -> Either String Replay.Replay
+  :: Bool
+  -> Bool
+  -> ByteString.ByteString
+  -> Either ([String], Exception.SomeException) Replay.Replay
 decodeReplayFile fast = ByteGet.run . Replay.byteGet fast
 
 -- | Encodes a replay as JSON.
@@ -22,8 +28,12 @@
 encodeReplayJson = Json.encodePretty
 
 -- | Parses a JSON replay.
-decodeReplayJson :: ByteString.ByteString -> Either String Replay.Replay
-decodeReplayJson = Json.decode
+decodeReplayJson
+  :: ByteString.ByteString
+  -> Either ([String], Exception.SomeException) Replay.Replay
+decodeReplayJson =
+  Bifunctor.first ((,) [] . Exception.toException . InvalidJson.InvalidJson)
+    . Json.decode
 
 -- | Encodes a raw replay.
 encodeReplayFile :: Bool -> Replay.Replay -> LazyByteString.ByteString
