louthy/language-ext · error · ExceptionalException

-2000000009

-2000000009

Error message

The IO monad is not in the monad-transformer stack or MonadIO.ToIO has not been implemented in the trait implementation for your monad-type.  Therefore it's not possible to leverage `MonadIO` unlifting trait functionality. To resolve this, implement `MonadIO.ToIO` and/or `MonadIO.MapIO`.

What it means

MonadUnliftIO.ToIOMaybe's default implementation throws ExceptionalException (code -2000000009) because the monad M (or its stack) does not implement MonadIO.ToIO/MapIO, so the library cannot extract the IO monad from within M. Unlifting requires explicit support in the trait implementation.

Solutions

  1. Override MonadIO.ToIO and/or MonadIO.MapIO in the trait implementation for M
  2. Restructure so the IO monad is the outermost layer and can be run directly
  3. Replace unlift-style code with lift-style code (lift the final effect into M)

Example fix

// before
var io = ToIOMaybe(ma); // ExceptionalException -2000000009
// after
// in trait impl for M: public K<M, IO<A>> ToIO<A>(K<M, A> ma) => Map(ma, IO.pure);
Defensive patterns

Strategy: try-catch

Validate before calling

// static check: MonadIO<M> overrides ToIO/MapIO before calling ToIOMaybe

Type guard

bool CanUnliftIO<M>() => typeof(MonadIO<M>).GetMethod("ToIO")!.DeclaringType != typeof(MonadIO<M>);

Try / catch

try { return ToIOMaybe(ma); }
catch (ExceptionalException ex) when (ex.Code == Errors.ToIONotSupported.Code) { /* restructure to lift-style */ }

Prevention

When it happens

Trigger: Calling ToIOMaybe<A>(K<M, A>) (or MapIOMaybe) on a monad whose trait impl lacks ToIO/MapIO overrides.

Common situations: Trying to run an IO action embedded inside a custom monad or transformer stack without an IO layer; generic monadic code that assumes unlift capability.

Related errors


AI-assisted analysis of louthy/language-ext@2f0e362824 (2026-09-15). Data as JSON: /api/errors/1772433d2169259b. Report an issue: GitHub.

Appendix: source

Thrown at LanguageExt.Core/Traits/Maybe Traits/MonadUnliftIO/MonadUnliftIO.Trait.cs:30

    public interface MonadUnliftIO<M> : Maybe.MonadIO<M>
        where M : MonadUnliftIO<M>, Traits.Monad<M>
    {
        /// <summary>
        /// Extract the IO monad from within the M monad (usually as part of a monad-transformer stack).
        /// </summary>
        /// <remarks>
        /// IMPLEMENTATION REQUIRED: If this method isn't overloaded in this monad
        /// or any monad in the stack on the way to the inner-monad, then it will throw
        /// an exception.
        ///
        /// This isn't ideal, it appears to be the only way to achieve this
        /// kind of functionality in C# without resorting to magic. 
        /// </remarks>
        /// <exception cref="ExceptionalException">If this method isn't overloaded in
        /// the inner monad or any monad in the stack on the way to the inner-monad,
        /// then it will throw an exception.</exception>
        public static virtual K<M, IO<A>> ToIOMaybe<A>(K<M, A> ma) =>
            throw new ExceptionalException(Errors.ToIONotSupported);

        /// <summary>
        /// Extract the IO monad from within the `M` monad (usually as part of a monad-transformer stack).  Then perform
        /// a mapping operation on the IO action before lifting the IO back into the `M` monad.
        /// </summary>
        public static virtual K<M, B> MapIOMaybe<A, B>(K<M, A> ma, Func<IO<A>, IO<B>> f) =>
            M.ToIOMaybe(ma).Bind(io => M.LiftIOMaybe(f(io)));

        /// <summary>
        /// Queue this IO operation to run on the thread-pool. 
        /// </summary>
        /// <param name="timeout">Maximum time that the forked IO operation can run for. `None` for no timeout.</param>
        /// <returns>Returns a `ForkIO` data-structure that contains two IO effects that can be used to either cancel
        /// the forked IO operation or to await the result of it.
        /// </returns>
        public static virtual K<M, ForkIO<A>> ForkIOMaybe<A>(K<M, A> ma, Option<TimeSpan> timeout = default) =>
            M.MapIOMaybe(ma, io => io.Fork(timeout));

View on GitHub (pinned to 2f0e362824)