Skip to content
Dimensa

Next.js debugging

Form says success, but nothing is saved? Prove where the data stops

The visitor fills in the form, clicks submit and sees a green message. No error in the console, no failed request. Weeks later you look for the submissions, and there are none.

We hit this bug twice in our own builds, in two different forms. Both times the problem was the same: the success message was not connected to the data actually being stored. This page shows how to prove where your data stops, and how to make sure it can never happen silently again.

Short answer

  • Open the Network tab and submit the form. No request at all means the success message is client-side only.
  • A request that returns 200 proves nothing about storage. It only proves the route returned. Look for the record in your database or store.
  • Read the record back. If you cannot find it, it was never saved, whatever the message said.
  • The fix: return success only after the write succeeded. If storage fails, return an error with a fallback, never a green message.

Version 1: success without a request

The first time was a waitlist on one of our own sites. The form checked that the email looked valid, printed "You are on the list" and threw the address away. No request was ever sent. There was no server code and no storage behind it. Every signup since launch was gone.

From the outside it looked perfect. That is exactly why nobody noticed: there was no error to see, because nothing ever tried to save anything.

Version 2: a 200 that saved nothing

The second time was the first version of the quote form on this site. This one did send a request. The API route validated the input and had a honeypot against bots. Then it only wrote the submission to console.log and returned { ok: true }.

So the browser got a real 200 from a real server. But the submission was not stored anywhere. A 200 only proves that your route ran to the end. It does not prove that the data landed.

How to prove where the data stops

Step 1: the browser. Open DevTools, go to the Network tab and submit the form. No request means the problem is in the client: the success message is shown without asking the server anything.

Step 2: the response. If there is a request, check its status and body. A 200 tells you the route finished. A 4xx or 5xx that the form still shows as success means the client ignores the response.

Step 3: the store. Find the record where it should be: the database table, the blob store, the CRM. Use the exact time of your test submission. If it is not there, the route returned without saving.

Step 4: the route. Read the route from top to bottom and find the line that writes the data. Then check what happens when that line fails. A catch that logs and still returns success is the same bug in disguise.

The fix: success comes from the write, not the button

The rule we now use on every form: store first, and return success only when the write has succeeded. If there is no store, or the write fails, the visitor gets an honest error with another way to reach you. Never a green message above a bin.

This is the shape of the quote route on this site today, shortened. The notification email is sent after the data is safe, and a failed email does not change the answer to the visitor.

export async function POST(req: Request) {
  const data = await validate(req); // 400 with field errors if invalid

  const saved = await save(data); // returns true ONLY if the write succeeded
  if (!saved) {
    console.error("[FORM][NOT SAVED]", JSON.stringify(data));
    return Response.json(
      { ok: false, error: "Saving failed. Please email us at hello@example.com." },
      { status: 503 },
    );
  }

  await notify(data).catch((err) => console.error("[FORM][MAIL FAILED]", err));
  return Response.json({ ok: true });
}

On the client: show success only on ok

The client side matters just as much. Show the success message only when the response is ok. Show an error, with an email address as fallback, for every other outcome: a 4xx, a 5xx, or a network error. Then test it: submit once, and read the record back from the store.

We test every form with at least three cases: a valid submission (200, and the record can be read back), an empty submission (400 with an error per field), and a filled honeypot (200, but nothing stored).

When this is not your problem

If the record is stored but the notification email never arrives, the data is safe and the problem is in mail delivery: the sending domain, spam filtering, or the mail provider. That is a different bug with a different fix. Check your store first, so you know which one you have.

Losing form submissions, or not sure?

Send us the URL of the form, what the visitor sees after submitting, and where the data should end up. We first prove where the data stops, then tell you what the fix takes.

Please do not send passwords or API keys. If we need access, we arrange that safely.

Request a quote

FAQ

Frequently asked questions

Why does my Next.js API route return 200 but the data is not saved?

A 200 only means the route finished. Often the write never happens, fails inside a catch that still returns success, or writes somewhere you are not looking. Read the record back from your store to know for sure.

My form shows a success message but there is no network request. Why?

Then the success message is client-side only. The form validates the input and shows the message without sending anything to a server. You need a real request to a route that stores the data.

Should I return 200 when saving fails?

No. Return an error status, such as 503, with a clear message and another way to reach you. Returning success after a failed write hides the problem from you and from the visitor.

How do I test a form properly?

Submit a valid entry and read the record back from the store, submit an empty form and check the field errors, and test what the visitor sees when the server returns an error or the network is down.