← All posts
·7 min read·GuardLayer

"Only async functions are allowed" in use server: safe fix

Next.jsServer ActionsSupabaseSecurity

Only async functions are allowed to be exported in a "use server" file means the file exports something that isn't a function: a constant, a class, a re-export or a default object. Move that export into a module without the directive. Don't wrap it in async function, because every export of a "use server" file becomes a Server Action anyone can call with a POST request.

The error reads like a type complaint, and the obvious fix is to make the compiler happy. In a "use server" file, that can publish an endpoint.

What does "Only async functions are allowed to be exported" mean?

It means an export in a "use server" file can never be a function. Next.js turns every export of such a file into a Server Action, so the SWC transform rejects anything else. In Next.js 16.4.3 the message comes from crates/next-custom-transforms/src/transforms/server_actions.rs, and the compiler's test fixtures show what triggers it:

"use server";
// Each of these, on its own, triggers the error:

export const x = 1;                // literal
export default class Component {} // class
export * from "./helpers";         // star re-export
export default 1;                  // non-function default
export default { a, b };           // default object

Three related messages mean different things:

MessageWhenCause
Only async functions are allowed to be exported in a "use server" file.Compile timeA literal, class, export *, re-export or non-function default
A "use server" file can only export async functions, found object.RuntimeAn exported object or array, such as a config const
Server Actions must be async functions.Compile timeAn exported function without async

The middle one is easy to miss. The compiler deliberately lets exported object and array literals through (a comment in the transform says rejecting them "would be too restrictive"). The transform then appends a runtime check, ensureServerEntryExports, which throws found object when the server loads the module. That check only tests typeof action !== "function"; the source says it "can only check that they are functions", so it doesn't enforce async. A "use cache" file produces the same compile message with "use cache" in place of "use server".

Why the quick fixes are risky

Two fixes come up often, and both make the error go away.

Fix 1: wrap the value in an async getter. export const PLAN_LIMITS = {...} becomes export async function getPlanLimits(). The build passes, and getPlanLimits is now a Server Action. For a constant that's harmless. The problem is what else lives in that file.

Fix 2: split the default object into named exports. In a Vercel community thread from October 2025 (Next.js 15.2.4), the poster had export default { getHomepageSections, updateSectionOrder, toggleSectionVisibility } in app/actions/homepage.ts. The answer was to export each function on its own, and it also suggested moving the read helper into a file without the directive. That second half is the part that matters: splitting the object turns each function into its own action, including the ones you thought of as internal.

The official error page doesn't warn about this. It explains that the functions "can be invoked directly from the client through a network request", then says to "make sure that they are all defined as async functions". Its "correct" example, export async function getServerData(), fixes the missing-async case, which actually produces the third message in the table. Nowhere does it suggest moving non-actions out of the file.

What the error looks like in a real file

A billing module holds plan limits and a setPlan helper that the Stripe webhook route calls once it has verified the signature. Someone adds "use server" to the file, believing it keeps the module on the server (it doesn't; see use server vs server-only). The plan limits object triggers found object, so it gets wrapped in a function:

"use server";

import { createClient } from "@supabase/supabase-js";

const admin = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

// Was `export const PLAN_LIMITS = { free: 3, pro: 50 }` until the
// "found object" error. Wrapped in a function to make it build.
export async function getPlanLimits() {
  return { free: 3, pro: 50 };
}

// Called by the Stripe webhook route after it verifies the signature.
export async function setPlan(userId: string, plan: "free" | "pro") {
  await admin.from("profiles").update({ plan }).eq("id", userId);
}
guardlayer scan · lib/billing.tsLive engine output
Passed with warnings
92/100 · A
  • Warninglib/billing.ts:18

    Server Action without auth check

    At the top of every Server Action, resolve and verify the current user (e.g. const { user } = await getUser(); if (!user) throw …) and authorize the specific operation before mutating data. If you do guard it with a custom helper, this warning is a false positive.

The error is gone, and setPlan is now a Server Action. The webhook route checks Stripe's signature before calling it, but the action itself checks nothing: it takes a userId and a plan from the caller and writes with a key that bypasses every RLS policy. Anyone who can invoke it can set their own account, or someone else's, to pro.

How reachable is it? The Next.js data security guide says an exported Server Action "is reachable via a direct POST request, not just through your application's UI". Next.js encrypts action IDs and drops unused actions from the client bundle during next build, but the same guide still says to "treat Server Actions as reachable via direct POST requests and verify authentication and authorization inside each one". Dead-code elimination is a build optimization, not access control.

How do I fix "Only async functions are allowed" safely?

Move every export that isn't a deliberate action out of the "use server" file. That's the fix for all three messages.

Constants and config go into a plain module:

// lib/plans.ts
export const PLAN_LIMITS = { free: 3, pro: 50 } as const;

Server-side helpers go into a module guarded by server-only, which fails the build if a client component imports it:

// lib/billing.ts
import "server-only";
import { createClient } from "@supabase/supabase-js";

const admin = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);

export async function setPlan(userId: string, plan: "free" | "pro") {
  await admin.from("profiles").update({ plan }).eq("id", userId);
}

The webhook route imports setPlan from there. Route handlers already run on the server and don't need the directive. The route stays the only entry point, and it verifies the Stripe signature before calling anything.

What's left in "use server" files should be things you're happy for any visitor to call, each with its own check: resolve the user with supabase.auth.getUser(), then use user.id from the session instead of an ID from the arguments. The auth check every Server Action needs covers that pattern.

If the message is Server Actions must be async functions, adding async fixes it and publishes the function. If it's a formatter, a validator or a data helper, move it out instead. For a default-export object, sort each function before splitting: mutations the UI calls stay, with auth checks, and everything else moves.

A quick self-check

# Non-function exports inside "use server" files
grep -rlE "^[\"']use server[\"']" app lib src 2>/dev/null \
  | xargs grep -nHE "^export (const|let|default \{|\*|\{)"

Then read every "use server" file's export list as an API surface: every export should be something you'd ship as a public endpoint.

GuardLayer's nextjs/server-action-no-auth rule flags a "use server" module that writes to the database with no detectable auth check, as in the scan above. It's a heuristic warning. It won't flag an exported read helper, so a getAllUsers() that selects every row through a service-role client in a "use server" file still needs your eyes.

FAQ

Why does Next.js require async functions in a "use server" file? Every export becomes a Server Action, which the client calls over the network, so the call is always asynchronous. Constants, classes and objects can't be called at all, so the compiler or the runtime check rejects them.

Is it safe to wrap a constant in an async function to fix the error? For a constant, usually: it only returns a value. The same move on a helper that writes data publishes that helper as an endpoint.

Why do I get "found object" at runtime but no build error? The compiler lets exported object and array literals through. A runtime check added by the transform rejects them when the module loads, with A "use server" file can only export async functions, found object.

Can I export types from a "use server" file? Yes. Type-only exports are erased and don't trigger the error. Exported values are what count.

See which of your Supabase tables anyone can read, free

Connect your Supabase project read-only. GuardLayer checks RLS, policies, grants and functions as they are right now, and gives you the SQL to fix each problem. It never reads your rows and can't change anything.

Check my Supabase project

Prefer to check code instead? Scan your repo, no signup.

Keep reading