Skip to content

Server actions should allow Promise-returning functions #83518

@mcrovero

Description

@mcrovero

Link to the code that reproduces this issue

https://github.com/mcrovero/reproduce-nextjs-effect-server-action

To Reproduce

  1. Start the application in development (next dev)
  2. open the browser (http://localhost:3000)
  3. look at server logs

Current vs. Expected behavior

Current behavior

The compiler only accepts Server Actions that are declared with the async keyword directly.

Exports that return a Promise but are produced via higher-order functions (e.g. runEffectAction) or other async functions not directly exported fail with:

Server Actions must be async functions.

This makes it impossible to export Server Actions ergonomically from libraries such as Effect.

Expected behavior

Any exported binding that is an async function or otherwise returns a Promise should be accepted as a Server Action.

Exported functions should work as long as the final export conforms to the async function contract.

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 24.5.0: Tue Apr 22 19:54:49 PDT 2025; root:xnu-11417.121.6~2/RELEASE_ARM64_T6000
  Available memory (MB): 16384
  Available CPU cores: 8
Binaries:
  Node: 20.19.1
  npm: 10.8.2
  Yarn: N/A
  pnpm: 10.12.4
Relevant Packages:
  next: 15.5.1-canary.30 // Latest available version is detected (15.5.1-canary.30).
  eslint-config-next: N/A
  react: 19.1.1
  react-dom: 19.1.1
  typescript: 5.9.2
Next.js Config:
  output: N/A

Which area(s) are affected? (Select all that apply)

Server Actions

Which stage(s) are affected? (Select all that apply)

next dev (local)

Additional context

Examples of possible usecases:

"use server";

async function getData(input: number) {
  return { data: "done " + input };
}

// This is not a valid Server Action
export const doSomethingPromise = (input: number) => getData(input);
// Workaround
export const doSomethingPromise = async (input: number) => await getData(input);



// --- Effect uscases ---
import { Effect } from "effect";

// Simple execution not allowed as a Server Action
export const doSomethingNoWrapper = (input: number) =>
  Effect.promise(() => getData(input)).pipe(Effect.runPromise);
// we can workaround it by adding the async keyword
export const doSomethingNoWrapper = async (input: number) =>
  await Effect.promise(() => getData(input)).pipe(Effect.runPromise);

// Most of the time we would run effects using a wrapper function
// There is no developer friendly way to run the wrapper with the async workaround

function runEffectAction<I, O>(
  effectFn: (args: I) => Effect.Effect<O, never, never>
) {
  return async (args: I): Promise<O> => {
    return await Effect.runPromise(effectFn(args));
  };
}

export const doSomethingEffect = runEffectAction((input: number) =>
  Effect.promise(() => getData(input))
);

// We'd need to write something like
export const doSomethingEffect = async (input: number) => await runEffectAction((input: number) =>
  Effect.promise(() => getData(input))
)(input);

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions