Founders treat empty states and error messages as the thing they will get to "after the real work is done." That framing is wrong. These moments are when users decide whether your app is real or broken. A blank screen with no data looks like a bug. A stack trace looks like a crash. A spinner that never resolves looks abandoned. None of those users come back.
This step is basic usability, not finishing touches.
Walk every core flow in your app and ask three questions at each screen:
| State | Question | Fix |
|---|
| Empty | What does the user see with zero data? | A friendly nudge with a clear next action |
| Loading | What does the user see while waiting? | A skeleton or message, not a silent spinner |
| Error | What does the user see when something fails? | A human sentence and a path forward |
Then break things on purpose. Clear your database. Throttle your network in DevTools. Type nonsense into every input. Submit empty forms. Click buttons twice in a row. Whatever crashes, fix. Whatever shows a confusing screen, rewrite.
Error copy is its own little discipline. "Something went wrong — try again in a moment" is human. "TypeError: Cannot read properties of undefined" is not. Write the message a tired user can act on at 11pm.
Bad: a blank table with no headers when the user has no data yet.
Good: "No projects yet. Create your first one to get started." with a button.
Bad: a red banner reading 500 Internal Server Error.
Good: "Something went wrong on our end. Try again in a moment, or contact support."
Bad: a spinner that runs forever when the network drops.
Good: a spinner that times out after 10 seconds and shows a retry button.
- Treating empty states as a design afterthought. Users hit them first.
- Showing raw stack traces or status codes. Translate to plain English.
- Letting forms submit twice. Disable the button while the request is in flight.
Walk every core flow from frontend connect, break each one on purpose, and ship empty, loading, and error states for every screen.
Every core screen has a designed empty state, a visible loading state, and a friendly error message. A network throttle, a cleared database, and a malformed input no longer make the app feel broken.
Founders treat empty states and error messages as the thing they will get to "after the real work is done." That framing is wrong. These moments are when users decide whether your app is real or broken. A blank screen with no data looks like a bug. A stack trace looks like a crash. A spinner that never resolves looks abandoned. None of those users come back.
This step is basic usability, not finishing touches.
Walk every core flow in your app and ask three questions at each screen:
| State | Question | Fix |
|---|
| Empty | What does the user see with zero data? | A friendly nudge with a clear next action |
| Loading | What does the user see while waiting? | A skeleton or message, not a silent spinner |
| Error | What does the user see when something fails? | A human sentence and a path forward |
Then break things on purpose. Clear your database. Throttle your network in DevTools. Type nonsense into every input. Submit empty forms. Click buttons twice in a row. Whatever crashes, fix. Whatever shows a confusing screen, rewrite.
Error copy is its own little discipline. "Something went wrong — try again in a moment" is human. "TypeError: Cannot read properties of undefined" is not. Write the message a tired user can act on at 11pm.
Bad: a blank table with no headers when the user has no data yet.
Good: "No projects yet. Create your first one to get started." with a button.
Bad: a red banner reading 500 Internal Server Error.
Good: "Something went wrong on our end. Try again in a moment, or contact support."
Bad: a spinner that runs forever when the network drops.
Good: a spinner that times out after 10 seconds and shows a retry button.
- Treating empty states as a design afterthought. Users hit them first.
- Showing raw stack traces or status codes. Translate to plain English.
- Letting forms submit twice. Disable the button while the request is in flight.
Walk every core flow from frontend connect, break each one on purpose, and ship empty, loading, and error states for every screen.
Every core screen has a designed empty state, a visible loading state, and a friendly error message. A network throttle, a cleared database, and a malformed input no longer make the app feel broken.