diff --git a/apps/openmw/mwlua/stats.cpp b/apps/openmw/mwlua/stats.cpp
index 040d96e288..49a3efa9d3 100644
--- a/apps/openmw/mwlua/stats.cpp
+++ b/apps/openmw/mwlua/stats.cpp
@@ -139,7 +139,7 @@ namespace MWLua
         {
             auto& stats = ptr.getClass().getCreatureStats(ptr);
             if(prop == "current")
-                stats.setLevel(value.as<int>());
+                stats.setLevel(LuaUtil::cast<int>(value));
         }
     };
 
@@ -179,7 +179,7 @@ namespace MWLua
         {
             auto& stats = ptr.getClass().getCreatureStats(ptr);
             auto stat = stats.getDynamic(index);
-            float floatValue = value.as<float>();
+            float floatValue = LuaUtil::cast<float>(value);
             if(prop == "base")
                 stat.setBase(floatValue);
             else if(prop == "current")
@@ -209,9 +209,9 @@ namespace MWLua
 
         float getModified(const Context& context) const
         {
-            auto base = get(context, "base", &MWMechanics::AttributeValue::getBase).as<float>();
-            auto damage = get(context, "damage", &MWMechanics::AttributeValue::getDamage).as<float>();
-            auto modifier = get(context, "modifier", &MWMechanics::AttributeValue::getModifier).as<float>();
+            auto base = LuaUtil::cast<float>(get(context, "base", &MWMechanics::AttributeValue::getBase));
+            auto damage = LuaUtil::cast<float>(get(context, "damage", &MWMechanics::AttributeValue::getDamage));
+            auto modifier = LuaUtil::cast<float>(get(context, "modifier", &MWMechanics::AttributeValue::getModifier));
             return std::max(0.f, base - damage + modifier); // Should match AttributeValue::getModified
         }
 
@@ -234,7 +234,7 @@ namespace MWLua
         {
             auto& stats = ptr.getClass().getCreatureStats(ptr);
             auto stat = stats.getAttribute(index);
-            float floatValue = value.as<float>();
+            float floatValue = LuaUtil::cast<float>(value);
             if(prop == "base")
                 stat.setBase(floatValue);
             else if(prop == "damage")
@@ -281,9 +281,9 @@ namespace MWLua
 
         float getModified(const Context& context) const
         {
-            auto base = get(context, "base", &MWMechanics::SkillValue::getBase).as<float>();
-            auto damage = get(context, "damage", &MWMechanics::SkillValue::getDamage).as<float>();
-            auto modifier = get(context, "modifier", &MWMechanics::SkillValue::getModifier).as<float>();
+            auto base = LuaUtil::cast<float>(get(context, "base", &MWMechanics::SkillValue::getBase));
+            auto damage = LuaUtil::cast<float>(get(context, "damage", &MWMechanics::SkillValue::getDamage));
+            auto modifier = LuaUtil::cast<float>(get(context, "modifier", &MWMechanics::SkillValue::getModifier));
             return std::max(0.f, base - damage + modifier); // Should match SkillValue::getModified
         }
 
@@ -315,7 +315,7 @@ namespace MWLua
         {
             auto& stats = ptr.getClass().getNpcStats(ptr);
             auto stat = stats.getSkill(index);
-            float floatValue = value.as<float>();
+            float floatValue = LuaUtil::cast<float>(value);
             if(prop == "base")
                 stat.setBase(floatValue);
             else if(prop == "damage")
diff --git a/apps/openmw/mwlua/types/actor.cpp b/apps/openmw/mwlua/types/actor.cpp
index d022c631b4..34dcc3fb78 100644
--- a/apps/openmw/mwlua/types/actor.cpp
+++ b/apps/openmw/mwlua/types/actor.cpp
@@ -236,11 +236,11 @@ namespace MWLua
             SetEquipmentAction::Equipment eqp;
             for (auto& [key, value] : equipment)
             {
-                int slot = key.as<int>();
+                int slot = LuaUtil::cast<int>(key);
                 if (value.is<Object>())
-                    eqp[slot] = value.as<Object>().id();
+                    eqp[slot] = LuaUtil::cast<Object>(value).id();
                 else
-                    eqp[slot] = value.as<std::string>();
+                    eqp[slot] = LuaUtil::cast<std::string>(value);
             }
             context.mLuaManager->addAction(std::make_unique<SetEquipmentAction>(context.mLua, obj.id(), std::move(eqp)));
         };
diff --git a/apps/openmw_test_suite/lua/test_lua.cpp b/apps/openmw_test_suite/lua/test_lua.cpp
index 566fefa726..57d8ee97a6 100644
--- a/apps/openmw_test_suite/lua/test_lua.cpp
+++ b/apps/openmw_test_suite/lua/test_lua.cpp
@@ -87,6 +87,23 @@ return {
         EXPECT_EQ(LuaUtil::toString(sol::make_object(mLua.sol(), "something")), "\"something\"");
     }
 
+    TEST_F(LuaStateTest, Cast)
+    {
+        EXPECT_EQ(LuaUtil::cast<int>(sol::make_object(mLua.sol(), 3.14)), 3);
+        EXPECT_ERROR(
+            LuaUtil::cast<int>(sol::make_object(mLua.sol(), "3.14")), "Value \"\"3.14\"\" can not be casted to int");
+        EXPECT_ERROR(LuaUtil::cast<std::string_view>(sol::make_object(mLua.sol(), sol::nil)),
+            "Value \"nil\" can not be casted to string");
+        EXPECT_ERROR(LuaUtil::cast<std::string>(sol::make_object(mLua.sol(), sol::nil)),
+            "Value \"nil\" can not be casted to string");
+        EXPECT_ERROR(LuaUtil::cast<sol::table>(sol::make_object(mLua.sol(), sol::nil)),
+            "Value \"nil\" can not be casted to sol::table");
+        EXPECT_ERROR(LuaUtil::cast<sol::function>(sol::make_object(mLua.sol(), "3.14")),
+            "Value \"\"3.14\"\" can not be casted to sol::function");
+        EXPECT_ERROR(LuaUtil::cast<sol::protected_function>(sol::make_object(mLua.sol(), "3.14")),
+            "Value \"\"3.14\"\" can not be casted to sol::function");
+    }
+
     TEST_F(LuaStateTest, ErrorHandling)
     {
         EXPECT_ERROR(mLua.runInNewSandbox("invalid.lua"), "[string \"invalid.lua\"]:1:");
diff --git a/components/lua/l10n.cpp b/components/lua/l10n.cpp
index 469962d568..46fc37f59c 100644
--- a/components/lua/l10n.cpp
+++ b/components/lua/l10n.cpp
@@ -61,20 +61,20 @@ namespace LuaUtil
         {
             // Argument values
             if (value.is<std::string>())
-                args.push_back(icu::Formattable(value.as<std::string>().c_str()));
+                args.push_back(icu::Formattable(LuaUtil::cast<std::string>(value).c_str()));
             // Note: While we pass all numbers as doubles, they still seem to be handled appropriately.
             // Numbers can be forced to be integers using the argType number and argStyle integer
             //     E.g. {var, number, integer}
             else if (value.is<double>())
-                args.push_back(icu::Formattable(value.as<double>()));
+                args.push_back(icu::Formattable(LuaUtil::cast<double>(value)));
             else
             {
-                Log(Debug::Error) << "Unrecognized argument type for key \"" << key.as<std::string>()
-                    << "\" when formatting message \"" << messageId << "\"";
+                Log(Debug::Error) << "Unrecognized argument type for key \"" << LuaUtil::cast<std::string>(key)
+                                  << "\" when formatting message \"" << messageId << "\"";
             }
 
             // Argument names
-            const auto str = key.as<std::string>();
+            const auto str = LuaUtil::cast<std::string>(key);
             argNames.push_back(icu::UnicodeString::fromUTF8(icu::StringPiece(str.data(), str.size())));
         }
         return std::make_pair(args, argNames);
diff --git a/components/lua/luastate.cpp b/components/lua/luastate.cpp
index 76c574a334..80a6b998aa 100644
--- a/components/lua/luastate.cpp
+++ b/components/lua/luastate.cpp
@@ -301,4 +301,25 @@ namespace LuaUtil
             return call(sol::state_view(obj.lua_state())["tostring"], obj);
     }
 
+    std::string internal::formatCastingError(const sol::object& obj, const std::type_info& t)
+    {
+        const char* typeName = t.name();
+        if (t == typeid(int))
+            typeName = "int";
+        else if (t == typeid(unsigned))
+            typeName = "uint32";
+        else if (t == typeid(size_t))
+            typeName = "size_t";
+        else if (t == typeid(float))
+            typeName = "float";
+        else if (t == typeid(double))
+            typeName = "double";
+        else if (t == typeid(sol::table))
+            typeName = "sol::table";
+        else if (t == typeid(sol::function) || t == typeid(sol::protected_function))
+            typeName = "sol::function";
+        else if (t == typeid(std::string) || t == typeid(std::string_view))
+            typeName = "string";
+        return std::string("Value \"") + toString(obj) + std::string("\" can not be casted to ") + typeName;
+    }
 }
diff --git a/components/lua/luastate.hpp b/components/lua/luastate.hpp
index 3bd27f2250..a610b3e26d 100644
--- a/components/lua/luastate.hpp
+++ b/components/lua/luastate.hpp
@@ -2,6 +2,7 @@
 #define COMPONENTS_LUA_LUASTATE_H
 
 #include <map>
+#include <typeinfo>
 
 #include <sol/sol.hpp>
 
@@ -129,15 +130,25 @@ namespace LuaUtil
     // String representation of a Lua object. Should be used for debugging/logging purposes only.
     std::string toString(const sol::object&);
 
+    namespace internal
+    {
+        std::string formatCastingError(const sol::object& obj, const std::type_info&);
+    }
+
+    template <class T>
+    decltype(auto) cast(const sol::object& obj)
+    {
+        if (!obj.is<T>())
+            throw std::runtime_error(internal::formatCastingError(obj, typeid(T)));
+        return obj.as<T>();
+    }
+
     template <class T>
     T getValueOrDefault(const sol::object& obj, const T& defaultValue)
     {
         if (obj == sol::nil)
             return defaultValue;
-        if (obj.is<T>())
-            return obj.as<T>();
-        else
-            throw std::logic_error(std::string("Value \"") + toString(obj) + std::string("\" has unexpected type"));
+        return cast<T>(obj);
     }
 
     // Makes a table read only (when accessed from Lua) by wrapping it with an empty userdata.
diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp
index f6758e92d7..23029913e2 100644
--- a/components/lua/scriptscontainer.cpp
+++ b/components/lua/scriptscontainer.cpp
@@ -79,33 +79,34 @@ namespace LuaUtil
             if (scriptOutput == sol::nil)
                 return true;
             sol::object engineHandlers = sol::nil, eventHandlers = sol::nil;
-            for (const auto& [key, value] : sol::table(scriptOutput))
+            for (const auto& [key, value] : cast<sol::table>(scriptOutput))
             {
-                std::string_view sectionName = key.as<std::string_view>();
+                std::string_view sectionName = cast<std::string_view>(key);
                 if (sectionName == ENGINE_HANDLERS)
                     engineHandlers = value;
                 else if (sectionName == EVENT_HANDLERS)
                     eventHandlers = value;
                 else if (sectionName == INTERFACE_NAME)
-                    script.mInterfaceName = value.as<std::string>();
+                    script.mInterfaceName = cast<std::string>(value);
                 else if (sectionName == INTERFACE)
-                    script.mInterface = value.as<sol::table>();
+                    script.mInterface = cast<sol::table>(value);
                 else
                     Log(Debug::Error) << "Not supported section '" << sectionName << "' in " << debugName;
             }
             if (engineHandlers != sol::nil)
             {
-                for (const auto& [key, fn] : sol::table(engineHandlers))
+                for (const auto& [key, handler] : cast<sol::table>(engineHandlers))
                 {
-                    std::string_view handlerName = key.as<std::string_view>();
+                    std::string_view handlerName = cast<std::string_view>(key);
+                    sol::function fn = cast<sol::function>(handler);
                     if (handlerName == HANDLER_INIT)
-                        onInit = sol::function(fn);
+                        onInit = fn;
                     else if (handlerName == HANDLER_LOAD)
-                        onLoad = sol::function(fn);
+                        onLoad = fn;
                     else if (handlerName == HANDLER_SAVE)
-                        script.mOnSave = sol::function(fn);
+                        script.mOnSave = fn;
                     else if (handlerName == HANDLER_INTERFACE_OVERRIDE)
-                        script.mOnOverride = sol::function(fn);
+                        script.mOnOverride = fn;
                     else
                     {
                         auto it = mEngineHandlers.find(handlerName);
@@ -118,13 +119,13 @@ namespace LuaUtil
             }
             if (eventHandlers != sol::nil)
             {
-                for (const auto& [key, fn] : sol::table(eventHandlers))
+                for (const auto& [key, fn] : cast<sol::table>(eventHandlers))
                 {
-                    std::string_view eventName = key.as<std::string_view>();
+                    std::string_view eventName = cast<std::string_view>(key);
                     auto it = mEventHandlers.find(eventName);
                     if (it == mEventHandlers.end())
                         it = mEventHandlers.emplace(std::string(eventName), EventHandlerList()).first;
-                    insertHandler(it->second, scriptId, fn);
+                    insertHandler(it->second, scriptId, cast<sol::function>(fn));
                 }
             }
 
@@ -280,7 +281,7 @@ namespace LuaUtil
             try
             {
                 sol::object res = LuaUtil::call(list[i].mFn, data);
-                if (res != sol::nil && !res.as<bool>())
+                if (res.is<bool>() && !res.as<bool>())
                     break;  // Skip other handlers if 'false' was returned.
             }
             catch (std::exception& e)
diff --git a/components/lua/serialization.cpp b/components/lua/serialization.cpp
index f1ee7c1aae..1952949d30 100644
--- a/components/lua/serialization.cpp
+++ b/components/lua/serialization.cpp
@@ -106,7 +106,7 @@ namespace LuaUtil
 
     bool BasicSerializer::serialize(BinaryData& out, const sol::userdata& data) const
     {
-        appendRefNum(out, data.as<ESM::RefNum>());
+        appendRefNum(out, cast<ESM::RefNum>(data));
         return true;
     }
 
diff --git a/components/lua/storage.cpp b/components/lua/storage.cpp
index 11ab186018..a14f839e8d 100644
--- a/components/lua/storage.cpp
+++ b/components/lua/storage.cpp
@@ -80,7 +80,7 @@ namespace LuaUtil
         if (values)
         {
             for (const auto& [k, v] : *values)
-                mValues[k.as<std::string>()] = Value(v);
+                mValues[cast<std::string>(k)] = Value(v);
         }
         if (mStorage->mListener)
             mStorage->mListener->sectionReplaced(mSectionName, values);
@@ -165,9 +165,9 @@ namespace LuaUtil
             sol::table data = deserialize(mLua, serializedData);
             for (const auto& [sectionName, sectionTable] : data)
             {
-                const std::shared_ptr<Section>& section = getSection(sectionName.as<std::string_view>());
-                for (const auto& [key, value] : sol::table(sectionTable))
-                    section->set(key.as<std::string_view>(), value);
+                const std::shared_ptr<Section>& section = getSection(cast<std::string_view>(sectionName));
+                for (const auto& [key, value] : cast<sol::table>(sectionTable))
+                    section->set(cast<std::string_view>(key), value);
             }
         }
         catch (std::exception& e)
diff --git a/components/lua/utilpackage.cpp b/components/lua/utilpackage.cpp
index 88cf7cb17a..392a3f7c7a 100644
--- a/components/lua/utilpackage.cpp
+++ b/components/lua/utilpackage.cpp
@@ -230,19 +230,19 @@ namespace LuaUtil
             util["bitOr"] = [](unsigned a, sol::variadic_args va)
             {
                 for (const auto& v : va)
-                    a |= v.as<unsigned>();
+                    a |= cast<unsigned>(v);
                 return a;
             };
             util["bitAnd"] = [](unsigned a, sol::variadic_args va)
             {
                 for (const auto& v : va)
-                    a &= v.as<unsigned>();
+                    a &= cast<unsigned>(v);
                 return a;
             };
             util["bitXor"] = [](unsigned a, sol::variadic_args va)
             {
                 for (const auto& v : va)
-                    a ^= v.as<unsigned>();
+                    a ^= cast<unsigned>(v);
                 return a;
             };
             util["bitNot"] = [](unsigned a) { return ~a; };
diff --git a/components/lua_ui/content.hpp b/components/lua_ui/content.hpp
index 2caa1ff8dc..c8bb82ecf3 100644
--- a/components/lua_ui/content.hpp
+++ b/components/lua_ui/content.hpp
@@ -78,7 +78,7 @@ namespace LuaUi
         {
             sol::object result = callMethod("indexOf", name);
             if (result.is<size_t>())
-                return fromLua(result.as<size_t>());
+                return fromLua(LuaUtil::cast<size_t>(result));
             else
                 return std::nullopt;
         }
@@ -86,7 +86,7 @@ namespace LuaUi
         {
             sol::object result = callMethod("indexOf", table);
             if (result.is<size_t>())
-                return fromLua(result.as<size_t>());
+                return fromLua(LuaUtil::cast<size_t>(result));
             else
                 return std::nullopt;
         }
diff --git a/components/lua_ui/element.cpp b/components/lua_ui/element.cpp
index 738bf5228b..ccf54ddec4 100644
--- a/components/lua_ui/element.cpp
+++ b/components/lua_ui/element.cpp
@@ -62,7 +62,7 @@ namespace LuaUi
                     destroyWidget(w);
                 return result;
             }
-            ContentView content(contentObj.as<sol::table>());
+            ContentView content(LuaUtil::cast<sol::table>(contentObj));
             result.resize(content.size());
             size_t minSize = std::min(children.size(), content.size());
             for (size_t i = 0; i < minSize; i++)
diff --git a/components/lua_ui/element.hpp b/components/lua_ui/element.hpp
index 8151b4f88c..ca4aaa3a5b 100644
--- a/components/lua_ui/element.hpp
+++ b/components/lua_ui/element.hpp
@@ -36,7 +36,7 @@ namespace LuaUi
 
     private:
         Element(sol::table layout);
-        sol::table layout() { return mLayout.as<sol::table>(); }
+        sol::table layout() { return LuaUtil::cast<sol::table>(mLayout); }
         static std::map<Element*, std::shared_ptr<Element>> sAllElements;
         void updateAttachment();
     };