Senior quality signals in a take-home
Reviewers run the error path first: truthful screens for every request, server-side validation, config from the environment, one-command setup, no dead files.
Predict first
Before you read: predict
A reviewer opens a candidate's ExamHall proctor-dashboard take-home and runs it against the mock API, which fails a seeded 20% of requests. She reloads the page 50 times: the incidents request failed on 10 loads, and 6 of the other 40 replies held no incidents. api.get resolves with the reply's list and rejects when the request fails. The incidents panel:
What does the incidents panel show across her 50 loads?
function IncidentsPanel() { const [incidents, setIncidents] = useState(null); useEffect(() => { api.get('/incidents') .then(setIncidents) .catch(() => setIncidents([])); }, []); if (incidents === null) return <p>Loading incidents…</p>; if (incidents.length === 0) return <p>No incidents reported</p>; return <ul>{incidents.map((i) => <li key={i.id}>{i.title}</li>)}</ul>;}The 30-second version
A reviewer runs a take-home's error path before its happy path. Every request needs loading, data, empty and error screens; a failure stored as empty data shows a false “nothing here”. The API validates each field itself, config that varies per deploy comes from environment variables, setup is one command, and every file is reachable from a script.
Reviewers run the error path first: truthful screens for every request, server-side validation, config from the environment, one-command setup, no dead files.
How it works
Step 1 of 16
Analogy: A passenger lift's acceptance test before handover
The installer rides the car up and down: it works. The inspector cuts the mains, to see whether the display says “out of service” rather than going dark. She reads the display while the car travels and while no call waits, overloads the car to see it refuse and say why, and checks that the floors were set at commissioning, not in the firmware. Then she starts it from one labelled switch and follows the machine-room diagram.
| At the lift's acceptance test | In the take-home |
|---|---|
| The installer's ride up and down | The happy path |
| “Out of service” on the display, not a dark panel | An error screen, not a blank or false-empty one |
| The display while travelling, and with no call waiting | The loading and empty screens |
| The car refusing an overload at the door | The API checking each field |
| Floors set at commissioning, not in the firmware | Config from the environment |
| One labelled switch; a diagram a stranger can follow | One-command setup; every file reachable |
Breaks down when: an inspector certifies against a written standard, while a take-home reviewer has none, and companies weigh these signals differently. Wrong inference: “there is one fixed checklist, so ticking it passes.”
In real terms: before you submit, run your own app against a failing mock and look at every screen.
Examples, run for real
EXH-2330 is a synthetic review of the ExamHall proctor-dashboard take-home. The reviewer reloads it 50 times against the mock API, which fails each request with probability 0.2 from a fixed seed. The candidates panel has no failure handler. The flags panel starts as an empty list and, on a failure, draws nothing. The re-submission, MR !2630, gives every panel one status value:
api.get(url).then((rows) => setState({ status: 'ready', rows }), () => setState({ status: 'error' }));
// 'loading' -> "Loading …"; 'error' -> "… could not load." + Retry; no rows -> its own message; else the list
EXH-2330 (as submitted) · seed 20261001 · 50 loads · each request fails with p = 0.2
candidates replies: 30 with rows, 10 empty, 10 failed -> shows 30 list · 10 blank · 10 stuck on "Loading…"
flags replies: 26 with rows, 10 empty, 14 failed -> shows 26 list · 24 blank
rejections nobody handled: 10
MR !2630 (one status per panel) · seed 20261001 · 50 loads · each request fails with p = 0.2
candidates replies: 30 with rows, 10 empty, 10 failed -> shows 30 list · 10 "No candidates checked in yet" · 10 "Candidates could not load." + Retry
flags replies: 26 with rows, 10 empty, 14 failed -> shows 26 list · 14 "Flags could not load." + Retry · 10 "No flags raised"
rejections nobody handled: 0
As submitted, none of the 24 failed requests produced a screen that said so. The candidates panel froze on its loading text after each failure. The flags panel was blank 24 times: a proctor cannot tell its 14 failures from its 10 quiet replies. After MR !2630 the same seed gives four distinct screens, and every failure says what failed.
A real reviewer may weigh these screens differently.
Try it
Answer first, then check. Nothing is saved.
Question 1 of 6
A chat take-home's sidebar lists the user's conversations. To see a slow network, the reviewer holds back the mock's reply to /conversations; once she lets it go, it brings 3 conversations. api.get resolves with the reply's list and rejects when the request fails. The sidebar:
What does the sidebar show while the reply is held back?
function ConversationList() { const [conversations, setConversations] = useState([]); const [failed, setFailed] = useState(false); useEffect(() => { api.get('/conversations') .then(setConversations) .catch(() => setFailed(true)); }, []); if (failed) return <p>Conversations could not load. <button>Retry</button></p>; if (conversations.length === 0) return <p>No conversations yet</p>; return <ul>{conversations.map((c) => <li key={c.id}>{c.name}</li>)}</ul>;}Question 2 of 6
A chat take-home lets each user set quiet hours, when no notifications are sent. Its settings form refuses an end before a start. The reviewer reads its API handler:
Which check does her review ask the candidate to add?
app.put('/me/quiet-hours', (req, res) => { const { from, until, note } = req.body; if (Number.isNaN(Date.parse(from)) || Number.isNaN(Date.parse(until))) return res.status(400).json({ error: 'from and until must be timestamps' }); if (typeof note !== 'string' || note.length > 80) return res.status(400).json({ error: 'note must be text, at most 80 characters' }); quietHours.save(req.user.id, { from, until, note }); res.status(204).end();});Question 3 of 6
A chat take-home ships one build to two deploys, a staging app and the live app; the two differ only in their host name. Its src/config.js:
Under Twelve-Factor, which of the four must be read from the environment?
export const ROUTES = { '/': Inbox, '/c/:id': Conversation, '/settings': Settings };export const HANDLERS = { send: onSend, read: onRead }; // which function handles each eventexport const PAGE_SIZE = 50; // messages per screenexport const PUBLIC_URL = 'https://chat.example'; // used in invite linksQuestion 4 of 6
A chat take-home's repository, each file with its imports:
package.json "start": "node server/main.js", "test": "node --test test/
server/main.js import './http.js'
server/http.js import './routes/messages.js'
server/routes/messages.js import '../store.js', '../validate.js'
server/store.js (no imports)
server/validate.js (no imports)
server/export/transcript.js import './pdf.js'
server/export/pdf.js import './fonts.js'
server/export/fonts.js (no imports)
test/messages.test.js import '../server/routes/messages.js'
How many of these .js files are dead code that a stranger reads for nothing?
Question 5 of 6
A chat take-home's message pane. The reviewer's mock has four cases: a reply held back (a slow network), a failed request, an empty conversation, and a conversation with messages. api.get resolves with the reply's list and rejects when the request fails. Four versions of the pane:
Which version gives each of the four cases its own screen, and a screen that is true to that case?
function Version1() { const [state, setState] = useState({ status: 'loading' }); useEffect(() => { api.get('/messages').then( (rows) => setState(rows.length ? { status: 'ready', rows } : { status: 'error' }), () => setState({ status: 'error' })); }, []); if (state.status === 'loading') return <p>Loading messages…</p>; if (state.status === 'error') return <p>Messages could not load</p>; return <ul>{state.rows.map((m) => <li key={m.id}>{m.text}</li>)}</ul>;}function Version2() { const [messages, setMessages] = useState([{ id: 'hello', text: 'Say hello to start the chat' }]); const [failed, setFailed] = useState(false); useEffect(() => { api.get('/messages').then(setMessages, () => setFailed(true)); }, []); if (failed) return <p>Messages could not load</p>; if (messages.length === 0) return <p>No messages yet</p>; return <ul>{messages.map((m) => <li key={m.id}>{m.text}</li>)}</ul>;}function Version3() { const [messages, setMessages] = useState([]); const [loading, setLoading] = useState(true); const [failed, setFailed] = useState(false); useEffect(() => { api.get('/messages').then(setMessages, () => setFailed(true)).finally(() => setLoading(false)); }, []); if (loading) return <p>Loading messages…</p>; if (failed) return <p>Messages could not load</p>; if (messages.length === 0) return <p>No messages yet</p>; return <ul>{messages.map((m) => <li key={m.id}>{m.text}</li>)}</ul>;}function Version4() { const [messages, setMessages] = useState(null); const [failed, setFailed] = useState(false); useEffect(() => { api.get('/messages').then(setMessages, () => setFailed(true)); }, []); if (failed) return <p>Messages could not load</p>; if (!messages || messages.length === 0) return <p>Loading messages…</p>; return <ul>{messages.map((m) => <li key={m.id}>{m.text}</li>)}</ul>;}Question 6 of 6
A chat take-home exports a conversation as a PDF by calling execFile('wkhtmltopdf', …), a program installed system-wide on the candidate's laptop with Homebrew. The README's setup is one command, npm install && npm start, and the reviewer's machine has Node, npm and Homebrew.
Which way of making the PDF fits a twelve-factor setup?
Go deeper
Pro tip
Before you push, start the app once with the mock failing every request and once with every reply held, and read each screen aloud.
Related lessons
- Read first: Scope and time-box a take-home
- Related: The README as a design doc
- Related: Types vanish at runtime, validate at the boundary
- Related: Semantic HTML, the accessibility tree and ARIA
- Related: Focus and keyboard management
- Related: Durable client retries and cancellation
- Related: CI/CD pipelines, build once and promote everywhere
- Related: Testing judgment: confidence per cost
- Related: Test pyramid and testing trophy
- Related: Testing strategy under a time-box
Checked against the tools and docs as of 2026-10-01.