This is a small, easy detail to get backwards, and getting it backwards doesn't throw a clear error, it just means one of your two values silently isn't what you expected it to be.
The Common Pattern This Applies To
A Server Action used directly as a form's action prop automatically receives FormData as its argument. Sometimes you also need to pass something extra, an ID identifying which specific record this particular form instance belongs to, that isn't itself a field in the form.
// components/DeletePostButton.tsx
import { deletePost } from '@/actions/posts';
export function DeletePostButton({ postId }: { postId: string }) {
return (
<form action={deletePost.bind(null, postId)}>
<button type="submit">Delete</button>
</form>
);
}
// actions/posts.ts
'use server';
export async function deletePost(postId: string, formData: FormData) {
await Post.findByIdAndDelete(postId);
revalidatePath('/dashboard/posts');
}
This is the correct, standard pattern, and it hinges entirely on getting the parameter order right in both places, matching each other exactly.
Where This Actually Goes Wrong
.bind(null, postId) pre-fills the function's arguments starting from the left, the first parameter position, not appended after whatever Next.js would normally pass in. postId becomes the function's first argument, and formData, which Next.js still automatically supplies, becomes the second, shifted over by exactly one position.
// ❌ Function signature doesn't match the actual argument order .bind() produces
export async function deletePost(formData: FormData, postId: string) {
// formData here is actually receiving postId's value
// postId here is actually receiving formData
console.log(postId); // logs a FormData object, not the string you expected
}
Swap the parameter order in the function signature so it doesn't match how .bind() actually supplies arguments, and both values silently land in the wrong place. No error gets thrown, since both parameters exist and TypeScript, unless you're being genuinely careful with the types here, often won't catch this either, since FormData and a bound string are both just values being passed to a function expecting some combination of two arguments.
Why This Specific Mistake Is Easy to Make
The natural, intuitive mental model is "the form gives me formData, and I'm adding an extra value on top of that," which suggests formData first, extra value second, matching the order you'd think about it in conversation. .bind()'s actual behavior, prepending arguments from the left, runs counter to that intuition, and the correct order, bound value first, formData second, only becomes obvious once you specifically know how .bind() works, not from how the situation naturally gets described out loud.
How This Actually Manifests as a Bug
Depending on what your function does with each parameter, this can fail in different ways. If postId is used directly as a database ID and it's actually receiving a FormData object instead, a database query with a malformed ID typically does throw a real, if somewhat confusing, error, which at least surfaces the problem, even if not obviously. If the mismatched values happen to both be used in ways that don't immediately throw, string interpolation, a loose comparison, the bug can produce quietly wrong behavior with no error at all, which is the more dangerous version, since nothing points you toward the actual cause.
The Fix: Match the Order Deliberately, and Consider Typing It Explicitly
// actions/posts.ts
'use server';
export async function deletePost(postId: string, formData: FormData) {
await Post.findByIdAndDelete(postId);
revalidatePath('/dashboard/posts');
}
Keeping the function signature's argument order matching exactly how .bind() supplies them, bound values first, in the order they were bound, formData last, is the actual fix. For extra safety, especially on a function you're not confident everyone touching the codebase will get right by memory, a comment directly above the signature noting the expected call pattern removes any ambiguity for the next person editing it.
// Called as: deletePost.bind(null, postId) — postId first, formData supplied automatically after
export async function deletePost(postId: string, formData: FormData) {
// ...
}
Binding Multiple Extra Arguments
The same left-to-right rule extends cleanly to more than one bound value, in the exact order they're passed to .bind():
<form action={updatePost.bind(null, postId, currentUserId)}>
export async function updatePost(postId: string, userId: string, formData: FormData) {
// postId first, userId second, formData last, matching the bind() call exactly
}
The Actual Rule
.bind() prepends arguments from the left, in the order you pass them, and whatever Next.js automatically supplies, formData for a form action, always ends up last, after every explicitly bound value. The function signature needs to match that exact order, not the order that feels most natural to describe the situation in conversation. When in doubt, a one-line comment above the function noting the expected .bind() call pattern is cheap insurance against exactly this kind of easy-to-miss, hard-to-notice mistake.
I run into this pattern constantly building the SaaS dashboards and templates I sell at pixelanas.com, and it's exactly the kind of small detail worth getting right once and documenting, rather than re-deriving from memory every time a new form needs an extra bound argument.
If you've got a Server Action bound with extra arguments, worth double-checking the parameter order actually matches, especially on anything where a mismatch wouldn't throw an obvious error. Drop what you find in the comments.
Get the templates: https://pixelanas.gumroad.com
Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751
Top comments (0)