diff --git a/System/OsString/Common.hs b/System/OsString/Common.hs
--- a/System/OsString/Common.hs
+++ b/System/OsString/Common.hs
@@ -38,8 +38,10 @@
   , fromString
 #endif
   , fromBytes
+  , fromShortBytes
 #ifndef WINDOWS
   , fromBytestring
+  , fromShortBytestring
 #endif
   , pstr
   , singleton
@@ -158,6 +160,8 @@
     ( MonadThrow, throwM )
 import Data.ByteString.Internal
     ( ByteString )
+import Data.ByteString.Short.Internal
+    ( ShortByteString )
 import Control.Exception
     ( SomeException, try, displayException )
 import Control.DeepSeq ( force )
@@ -418,12 +422,33 @@
 fromBytes :: MonadThrow m
           => ByteString
           -> m PLATFORM_STRING
+fromBytes = fromShortBytes . BS16.toShort
+
+#ifdef WINDOWS_DOC
+-- | Constructs a platform string from a ShortByteString.
+--
+-- This ensures valid UCS-2LE.
+-- Note that this doesn't expand Word8 to Word16 on windows, so you may get invalid UTF-16.
+--
+-- Throws 'EncodingException' on invalid UCS-2LE (although unlikely).
+--
+-- @since 2.0.8
+#else
+-- | Constructs a platform string from a ShortByteString.
+--
+-- This is a no-op.
+--
+-- @since 2.0.8
+#endif
+fromShortBytes :: MonadThrow m
+               => ShortByteString
+               -> m PLATFORM_STRING
 #ifdef WINDOWS
-fromBytes bs =
-  let ws = WindowsString . BS16.toShort $ bs
+fromShortBytes bs =
+  let ws = WindowsString bs
   in either throwM (const . pure $ ws) $ decodeWith ucs2le ws
 #else
-fromBytes = pure . PosixString . BSP.toShort
+fromShortBytes = pure . PosixString
 #endif
 
 #ifndef WINDOWS
@@ -438,6 +463,12 @@
 -- @since 2.0.6
 fromBytestring :: ByteString -> PosixString
 fromBytestring = PosixString . BSP.toShort
+
+-- | Like 'fromShortBytes', but not in IO, similarly to 'fromBytestring'
+--
+-- @since 2.0.8
+fromShortBytestring :: ShortByteString -> PosixString
+fromShortBytestring = PosixString
 #endif
 
 
diff --git a/System/OsString/Internal.hs b/System/OsString/Internal.hs
--- a/System/OsString/Internal.hs
+++ b/System/OsString/Internal.hs
@@ -14,6 +14,8 @@
     ( MonadThrow )
 import Data.ByteString
     ( ByteString )
+import Data.ByteString.Short
+    ( ShortByteString )
 import Data.Char
 import Language.Haskell.TH.Quote
     ( QuasiQuoter (..) )
@@ -170,7 +172,17 @@
           -> m OsString
 fromBytes = fmap OsString . PF.fromBytes
 
+-- | Constructs an @OsString@ from a ShortByteString.
+--
+-- On windows, this ensures valid UCS-2LE, on unix it is passed unchanged/unchecked.
+--
+-- Throws 'EncodingException' on invalid UCS-2LE on windows (although unlikely).
+fromShortBytes :: MonadThrow m
+               => ShortByteString
+               -> m OsString
+fromShortBytes = fmap OsString . PF.fromShortBytes
 
+
 -- | QuasiQuote an 'OsString'. This accepts Unicode characters
 -- and encodes as UTF-8 on unix and UTF-16 on windows.
 -- If used as pattern, requires turning on the @ViewPatterns@ extension.
@@ -303,6 +315,10 @@
 null = coerce PF.null
 
 -- | /O(1)/ The length of a 'OsString'.
+--
+-- This returns the number of code units
+-- (@Word8@ on unix and @Word16@ on windows), not
+-- bytes.
 --
 -- @since 1.4.200.0
 length :: OsString -> Int
diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,9 @@
 # Changelog for [`os-string` package](http://hackage.haskell.org/package/os-string)
 
+## 2.0.8 *Aug 2025*
+
+* Add safe conversions from ShortByteString [#34](https://github.com/haskell/os-string/pull/34)
+
 ## 2.0.7 *Nov 2024*
 
 * don't catch asynchronous exceptions during encoding/decoding wrt [#22](https://github.com/haskell/os-string/issues/22)
diff --git a/os-string.cabal b/os-string.cabal
--- a/os-string.cabal
+++ b/os-string.cabal
@@ -1,6 +1,6 @@
 cabal-version:      2.2
 name:               os-string
-version:            2.0.7
+version:            2.0.8
 
 -- NOTE: Don't forget to update ./changelog.md
 license:            BSD-3-Clause
@@ -64,7 +64,7 @@
 
   default-language: Haskell2010
   build-depends:
-    , base              >=4.12.0.0      && <4.22
+    , base              >=4.12.0.0      && <4.23
     , bytestring        >=0.11.3.0
     , deepseq
     , exceptions
diff --git a/tests/encoding/EncodingSpec.hs b/tests/encoding/EncodingSpec.hs
--- a/tests/encoding/EncodingSpec.hs
+++ b/tests/encoding/EncodingSpec.hs
@@ -40,7 +40,9 @@
       let str = [toEnum 55296, toEnum 55297]
           encoded = encodeWithTE utf16le str
           decoded = decodeWithTE utf16le =<< encoded
-#if __GLASGOW_HASKELL__ >= 910
+#if __GLASGOW_HASKELL__ >= 912
+      in decoded === Left (EncodingError ("recoverEncode: invalid argument (cannot encode character " <> show (head str) <> ")") Nothing))
+#elif __GLASGOW_HASKELL__ >= 910
       in decoded === Left (EncodingError ("recoverEncode: invalid argument (cannot encode character " <> show (head str) <> ")\n") Nothing))
 #elif __GLASGOW_HASKELL__ >= 904
       in decoded === Left (EncodingError ("recoverEncode: invalid argument (cannot encode character " <> show (head str) <> ")") Nothing))
@@ -72,7 +74,9 @@
       let str = [toEnum 0xDFF0, toEnum 0xDFF2]
           encoded = encodeWithTE (mkUTF8 RoundtripFailure) str
           decoded = decodeWithTE (mkUTF8 RoundtripFailure) =<< encoded
-#if __GLASGOW_HASKELL__ >= 910
+#if __GLASGOW_HASKELL__ >= 912
+      in decoded === Left (EncodingError ("recoverEncode: invalid argument (cannot encode character " <> show (head str) <> ")") Nothing))
+#elif __GLASGOW_HASKELL__ >= 910
       in decoded === Left (EncodingError ("recoverEncode: invalid argument (cannot encode character " <> show (head str) <> ")\n") Nothing))
 #elif __GLASGOW_HASKELL__ >= 904
       in decoded === Left (EncodingError ("recoverEncode: invalid argument (cannot encode character " <> show (head str) <> ")") Nothing))
