When a route-handled write finishes, when should it return field errors as data and when should it redirect instead?
answer
- expected outcome versus crash
- the user must stay in the form
- the last history entry decides reload
- Post/Redirect/Get
- 303 turns the follow-up into a GET
basics
~20 sReturn data when the user must stay in the form: field-level errors rendered back into the same route, with their input preserved. Redirect on success, so the result is a plain GET that can be reloaded and bookmarked without resubmitting.
solid answer
~50 sA failed submission is an expected outcome, not a crash, so the handler should return it as a value the route renders — per-field messages beside the inputs, with the submitted values echoed back so nothing is retyped. Throwing instead sends the user to the nearest error screen and loses the form. A successful write should answer with a redirect, which is the Post/Redirect/Get pattern: `303 See Other` is defined to turn the follow-up into a `GET` whatever the original method was, so the browser ends up on a normal page. That matters because the alternative leaves the last history entry pointing at a `POST`: reloading it re-submits, and the browser has to warn about it. Redirecting also gives you a URL worth sharing, and lets you land the user somewhere other than the form they came from.
go deeper
Remember the split: bad input comes back as data so the form can show it, and a successful write ends in a redirect so the user lands on a normal page they can reload.
Explain the history mechanics — a rendered POST response leaves a POST entry, which is what makes reload re-submit — and know that 303 is defined to convert the follow-up to a GET while 307 and 308 deliberately do not.
Show the operational consequences: expected rejections must not pollute error reporting, submitted values must survive the re-render, and the outcome must be expressed as data so the scripted and scriptless paths behave identically.
Make it a convention rather than a per-handler choice. Agree what counts as expected versus exceptional, where redirect targets are decided, and how error shapes stay uniform enough that forms across the app do not each invent their own.
## Two kinds of outcome, two shapes of response A write handler finishes in one of three states, and only one of them is a genuine failure: 1. **The submission was not acceptable** — a field is missing, malformed, or violates a rule the server owns, such as an address already being taken. This is an ordinary, expected result of letting users type. 2. **The write succeeded** — the state changed. 3. **Something broke** — the store was unreachable, a required service timed out. This is the only one that deserves to be thrown. The mechanism a meta-framework gives you maps cleanly onto that. The handler can **return a value**, which the framework makes available to the same route as it renders again, or it can **return a redirect**, which the framework turns into a redirect response the browser follows. ## Why rejections come back as data When the handler returns a value, the response to the `POST` is the same route, rendered on the server, with that value in hand. The form is still on screen. The page can show a message beside each offending field, echo back what the user typed, and put focus where the first problem is. None of that needs script: the server re-rendered the whole document. Throwing instead hands control to the route's nearest error state, which is designed for the unexpected. The user gets a generic failure screen, their input is gone, and "you left the postcode blank" is presented with the same weight as "the database is down". A useful rule of thumb: **if a competent user could hit it by typing, it is data; if only an operator can fix it, it is an exception.** Two details make the data path work in practice: - **Echo the submitted values.** Without script there is nothing in the browser holding them; the re-render is the only chance to put them back in the inputs. - **Key the messages to fields.** A single banner is easy to return and hard to act on when the form has fifteen inputs. Frameworks differ on the status code they attach to that re-render — some use a plain success status because a document was produced, others let you set a 4xx. Either renders; the choice matters mainly to non-browser callers and to logging. ## Why success redirects A browser remembers how it obtained the current document. If the answer to a `POST` is a rendered page, that history entry *is* the `POST`: - pressing reload re-sends the submission, which is why browsers interrupt with a confirmation prompt; - going back and forward again can land on the same entry and pose the same question; - the URL in the bar is the form's target, which is rarely a URL anyone wants to share. Answering with a redirect replaces that entry. The browser issues a fresh `GET` to the target, and *that* is what lands in history. Reload is now harmless, back behaves normally, and the URL describes the thing the user is looking at. | Handler returns | Browser's last entry | Reload does | Good for | |---|---|---|---| | A value the route renders | the `POST` | re-submits, after a prompt | rejected input, staying in the form | | A redirect | a `GET` at the target | re-fetches the target | a successful write | ## The status codes involved Only a few matter: - **`303 See Other`** is the one defined for exactly this: follow the `Location` header with a `GET`, whatever method you started with. - **`302 Found`** is treated the same way by browsers in practice, which is why the pattern worked for years before 303 was widely used, but its specification does not require the method change. - **`307` and `308`** deliberately preserve the method, so they re-post. They are the wrong choice here. ## The seam between the two paths After hydration the framework usually intercepts the submit and posts in the background. It then applies the same two outcomes without a document load: a returned value is handed to the form for rendering, a redirect is performed as a client-side navigation. The design holds because the handler expressed the outcome as data, not as a DOM manipulation. That is also why the decision belongs in the handler rather than in the component that submitted: one expression of the outcome, two transports honouring it. ## Common mistakes - **Redirecting on a validation failure**, carrying the message in the query string. The user's typed values are gone and the message is now in a shareable URL. - **Returning a bare success flag** and letting the client decide where to go. The scriptless path has no client to decide, so it renders the form again with nothing to say. - **Answering with an empty response.** There is no script to interpret it before hydration, so the browser shows a blank document. - **Treating an expected rejection as a crash** so it lands in the error reporting pipeline alongside real incidents.
- Why does the browser prompt before reloading a page that was produced by a POST?Because reloading repeats the request that produced the document, and the browser cannot know whether repeating a POST is harmless. The prompt is the browser refusing to guess. Redirect-after-post removes the question entirely by making the entry a GET.
- What has to happen to the submitted values when the handler rejects them?They have to be echoed into the re-rendered form. Before hydration the browser holds nothing, so the server's render is the only place the inputs can be repopulated. Sensitive fields, such as a password, are the exception and are deliberately left blank.
saying these in an interview costs you the question
- Throws on a validation failure so the error screen replaces the form
- Redirects with the error message in the query string
- Says a POST result page is fine because users rarely reload
- Uses a method-preserving redirect and wonders why it re-posts
- Returns only a success boolean and leaves navigation to client code
- Answers a scriptless submit with an empty response body