diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
 
 `hslua-module-system` uses [PVP Versioning][].
 
+## hslua-module-system-1.2.1
+
+Released 2025-07-23.
+
+-   Add new function `exists`, which allows to check the existance
+    and, optionally, type of a filesystem object at the given
+    path.
+
 ## hslua-module-system-1.2.0
 
 Released 2025-06-23.
diff --git a/hslua-module-system.cabal b/hslua-module-system.cabal
--- a/hslua-module-system.cabal
+++ b/hslua-module-system.cabal
@@ -1,6 +1,6 @@
 cabal-version:       2.2
 name:                hslua-module-system
-version:             1.2.0
+version:             1.2.1
 synopsis:            Lua module wrapper around Haskell's System module.
 
 description:         Provides access to system information and
diff --git a/src/HsLua/Module/System.hs b/src/HsLua/Module/System.hs
--- a/src/HsLua/Module/System.hs
+++ b/src/HsLua/Module/System.hs
@@ -25,6 +25,7 @@
   , cp
   , cputime
   , env
+  , exists
   , getenv
   , getwd
   , ls
@@ -84,6 +85,7 @@
       , cp
       , cputime
       , env
+      , exists
       , getenv
       , getwd
       , ls
@@ -232,6 +234,40 @@
   =#> functionResult (pushKeyValuePairs pushString pushString) "table"
         "A table mapping environment variable names to their value."
   #? "Retrieves the entire environment as a string-indexed table."
+
+-- | Check the existence of a file path.
+exists :: LuaError e => DocumentedFunction e
+exists = defun "exists"
+  ### (\fp mbType -> do
+          case T.toLower <$> mbType of
+            Nothing ->
+              -- any file type is fine
+              ioToLua $ Directory.doesPathExist fp
+            Just "directory" ->
+              -- must be a directory or a symlink pointing to one
+              ioToLua $ Directory.doesDirectoryExist fp
+            Just "file" ->
+              -- must be a file or a symlink pointing to one
+              ioToLua $ Directory.doesFileExist fp
+            Just "symlink" ->
+              -- must exist and be a symlink
+              ioToLua $ (&&) <$> Directory.doesPathExist fp
+                             <*> Directory.pathIsSymbolicLink fp
+            Just otherType ->
+              failLua $
+              "Unsupported filesystem object type: " <> T.unpack otherType)
+  <#> filepathParam "path" "file path to check"
+  <#> opt (textParam "type" "the required type of the filesystem object")
+  =#> functionResult pushBool "boolean"
+        "whether a filesystem object of type `type` exists at `path`."
+  #? T.unlines
+     [ "Check whether there exists a filesystem object at the given path."
+     , "If `type` is given and either *directory* or *file*`, then the"
+     , "function returns `true` if and only if the file system object has"
+     , "the given type, or if it's a symlink pointing to an object of that"
+     , "type. Passing *symlink* as type requires the path itself to be a"
+     , "symlink. Types other than those will cause an error."
+     ]
 
 -- | Return the current working directory as an absolute path.
 getwd :: LuaError e => DocumentedFunction e
diff --git a/test/test-system.lua b/test/test-system.lua
--- a/test/test-system.lua
+++ b/test/test-system.lua
@@ -95,6 +95,48 @@
     end)
   },
 
+  group 'exists' {
+    test('returns `false` if the path does not exist', in_tmpdir(function ()
+      -- the temporary dir should be empty
+      assert.is_falsy(system.exists('does-not-exist.txt'))
+    end)),
+    test('returns `true` if the path does not exist', in_tmpdir(function ()
+      io.open('README.md', 'w'):close()
+      assert.is_truthy(system.exists('README.md'))
+    end)),
+    test('returns `false` for non-existing files', in_tmpdir(function ()
+      assert.is_falsy(system.exists('README.md', 'file'))
+    end)),
+    test('returns `true` for existing files', in_tmpdir(function ()
+      io.open('README.md', 'w'):close()
+      assert.is_truthy(system.exists('README.md', 'file'))
+    end)),
+    test('returns `false` for missing directories', in_tmpdir(function ()
+      assert.is_falsy(system.exists('folder', 'directory'))
+    end)),
+    test('returns `true` for existing directories', in_tmpdir(function ()
+      system.mkdir 'folder'
+      assert.is_truthy(system.exists('folder', 'directory'))
+    end)),
+    test('returns `false` for missing directories', in_tmpdir(function ()
+      assert.is_falsy(system.exists('folder', 'directory'))
+    end)),
+    test('returns `false` for file when checking for dir', in_tmpdir(function ()
+      io.open('folder', 'w'):close()
+      assert.is_falsy(system.exists('folder', 'directory'))
+    end)),
+    test('returns `true` for dir when checking for file', in_tmpdir(function ()
+      system.mkdir 'README.md'
+      assert.is_falsy(system.exists('README.md', 'file'))
+    end)),
+    test('errors for unknown type', in_tmpdir(function ()
+      assert.error_matches(
+        function () system.exists('x', 'device') end,
+        'Unsupported filesystem object'
+      )
+    end)),
+  },
+
   group 'ls' {
     test('returns a table', function ()
       assert.are_equal(type(system.ls('.')), 'table')
