How to Test Error States and Empty States
The "happy path" is where most development and testing efforts are concentrated. We build features, we test them with valid data, and we ensure they function as intended under ideal conditions. But wh
Beyond the Happy Path: Mastering the Testing of Error and Empty States
The "happy path" is where most development and testing efforts are concentrated. We build features, we test them with valid data, and we ensure they function as intended under ideal conditions. But what happens when the network connection drops? When a backend service times out? When a user opens a brand-new feature for the first time with no data to display? These are the moments of truth, the critical junctures where applications can either gracefully recover and inform the user, or crumble into a frustrating, unrecoverable mess.
Testing error states and empty states is not merely an afterthought; it's a fundamental aspect of building robust, user-friendly applications. These states, often overlooked, directly impact user perception, retention, and the overall perceived quality of a product. A poorly handled error can lead to data loss, user abandonment, and a cascade of support tickets. Similarly, an empty state that fails to guide the user can leave them bewildered and disengaged.
This article will provide a comprehensive guide for engineers—both developers and QA professionals—on how to thoroughly test these often-neglected states. We will explore the nuances between different types of "empty" states, strategies for simulating and testing various error conditions, and the importance of user-centric messaging. We'll cover deterministic testing techniques, the value of adversarial testing personas, and practical approaches for both manual and automated testing.
The Spectrum of Non-Happy States
Before diving into testing strategies, it’s crucial to understand the distinct categories of states that deviate from the ideal "happy path." These states, while related, have unique characteristics and require tailored testing approaches.
#### Empty State vs. Zero State vs. First-Run State
The term "empty state" is often used as a catch-all, but there are subtle yet important distinctions:
- Empty State: This generally refers to a UI that displays no content because there is *no data available* to show, even though the feature has been used previously. For example, a user's "Favorites" list might be empty if they haven't favorited any items yet.
- Zero State: This is a more specific type of empty state where the absence of data is a *natural and expected consequence* of the system's current configuration or usage. A prime example is a newly created account with no transactions yet. The "zero" implies that the system is functioning correctly, but there simply isn't any data to populate it.
- First-Run State: This state occurs the *very first time a user interacts with a feature*. It might coincide with an empty state, but it also often includes onboarding elements, tutorials, or prompts to encourage initial engagement and data creation. A welcome message and a call to action to "Add your first item" is typical of a first-run state.
Failing to differentiate can lead to incorrect assumptions. For instance, showing a "No items found" message on a user's very first visit to their shopping cart (a first-run/empty state) might be confusing. A better approach would be a welcoming message explaining the cart's purpose and a prompt to add items.
#### Loading States: The Crucial Intermediary
Between fetching data and displaying it, there's the loading state. This is not an error, nor is it an empty state, but a temporary condition that signals to the user that something is happening.
- Spinners: The classic rotating icon. While familiar, spinners can sometimes be perceived as "dead" if they persist for too long, leading to user frustration.
- Skeleton Screens: These are UI placeholders that mimic the structure of the content that will eventually load. They provide a visual cue about what to expect and can often feel more engaging than a simple spinner, as they offer a glimpse of the final layout.
The effectiveness of a loading state depends on its duration and clarity. A prolonged or ambiguous loading state can feel like an error to the user.
#### Error States: When Things Go Wrong
Error states occur when an operation fails. These can range from minor network glitches to critical backend failures.
- Network Errors: Connection lost, timed out requests, unreachable servers.
- Server-Side Errors: 5xx errors from the API, database issues, internal server faults.
- Client-Side Errors: Invalid user input that cannot be processed, unexpected application crashes.
- Permission Errors: User lacks the necessary authorization to perform an action.
- Data Validation Errors: Data provided by the user fails to meet the application's requirements.
The core principle for testing error states is to simulate these failures deterministically and observe the application's response.
#### Partial States: Incomplete Information
Sometimes, an application might only be able to retrieve partial data due to various issues, such as a slow network, a partial API response, or a user canceling an operation mid-way. The UI needs to handle this gracefully, clearly indicating what data is available and what is missing, or allowing the user to refresh or retry.
The Motivation: Why Test These States Rigorously?
Neglecting these non-happy path states is a common pitfall that can have significant consequences.
#### User Experience and Retention
- Frustration and Abandonment: A user encountering an unhandled error or a confusing empty state is likely to become frustrated and abandon the application. This is particularly true for new users who are forming their initial impression.
- Loss of Trust: Repeated errors or poor error handling erode user trust in the application's reliability and data integrity.
- Increased Support Load: Unclear error messages or broken flows lead to a surge in customer support inquiries, increasing operational costs.
#### Data Integrity and Application Robustness
- Data Corruption: In some cases, improper handling of errors could lead to inconsistent or corrupted data.
- Crashes and ANRs: Unhandled exceptions during error conditions can cause application crashes or Android Not Responding (ANR) errors, severely impacting usability.
- Security Vulnerabilities: Sometimes, error messages can inadvertently reveal sensitive information about the application's internal workings if not carefully crafted.
#### Discovering Edge Cases
- Production Realities: Production environments are complex and unpredictable. Network conditions vary, backend services experience intermittent failures, and users interact with applications in ways developers might not anticipate. Testing these states helps uncover edge cases that might not surface during standard development testing.
- Adversarial Testing: Techniques that deliberately try to break the application by forcing error conditions can expose hidden bugs and dead ends.
Building a Comprehensive Test Matrix
A well-defined test matrix is the backbone of effective testing for error and empty states. It ensures that all critical scenarios are considered and systematically tested. This matrix should cover different states, the conditions that trigger them, the expected user-facing outcome, and the verification criteria.
Here’s a sample matrix, which can be expanded based on the specific features of your application.
#### Test Matrix: Error and Empty States
| Feature/Component | State Type | Triggering Condition | Expected User Experience | Verification Criteria |
|---|---|---|---|---|
| User Profile | Empty (First Run) | User opens profile for the first time, no data entered. | Welcoming message, prompt to "Complete your profile", clear fields to fill. | - Profile screen displays a welcoming message. - Fields for name, email, etc., are visible and editable. - "Save" or "Complete" button is present. - No "data not found" errors are shown. |
| User Profile | Empty (Post-Usage) | User clears all profile information. | "Your profile is currently empty. Add your details to personalize your experience." | - Profile screen displays a clear message indicating no data. - Prompt to add data is present. - No errors are displayed. |
| Product List | Empty | No products available in the selected category. | "No products found in this category. Try another category or check back later." | - Message clearly states no products. - Suggestion to try another category or check later. - Search/filter UI remains functional. - No generic error messages. |
| Product List | Loading | Fetching product data from API. | Skeleton screen or spinner indicating data is loading. | - Skeleton UI matches expected product card layout. - Spinner is visible and animates. - No "blank" screen or visible error. |
| Product List | Error | API request fails (e.g., 503 Service Unavailable). | "We're having trouble loading products right now. Please try again later." with a retry button. | - User-friendly error message. - Clear call to action (e.g., "Retry"). - Retry button is functional and triggers a new API request. - No raw stack traces or technical jargon. |
| Shopping Cart | Empty | User has not added any items to the cart. | "Your shopping cart is empty. Start shopping to add items!" with a link to the shop. | - Clear message indicating emptiness. - Call to action to start shopping is present. - Link to the shop page is functional. - "Checkout" button is disabled or hidden. |
| Shopping Cart | Error | Failed to load cart contents (e.g., network error). | "Could not load your cart. Please check your connection and try again." with a retry button. | - User-friendly error message. - Suggestion to check connection. - Retry button is present and functional. - No sensitive information revealed. |
| Checkout | Error | Payment gateway timeout. | "Payment failed. Please review your details and try again, or use a different payment method." | - Clear indication of payment failure. - Fields for payment details are still accessible. - Option to re-enter details or choose another method. - Order is not placed. |
| Checkout | Partial | Shipping address not fully validated. | "Please complete your shipping address to proceed." | - Specific fields requiring attention are highlighted. - User can edit the address. - "Continue" button is disabled until validation passes. |
| Settings | Error | Failed to save settings (e.g., backend error). | "Settings could not be saved. Please try again." | - Message indicating failure. - Original settings are still displayed. - "Save" button is active for retry. - No partial save or data inconsistency. |
| Notifications | Empty | No new notifications for the user. | "You have no new notifications." | - Clear, concise message. - No indication of an error. |
| Feed/Activity | Partial | Only some posts loaded due to network issues. | "Showing some recent activity. Tap to load more." | - Clearly indicates partial loading. - Provides an option to refresh or load more. - Previously loaded content remains visible. |
Strategies for Triggering and Testing States
The key to testing these states effectively lies in the ability to reliably trigger them. This often requires a combination of techniques.
#### Deterministic State Simulation
While some states occur naturally, many require deliberate manipulation to test.
##### Forcing Network Conditions
- Developer Tools: Most modern web browsers and mobile development environments offer tools to simulate various network conditions, including slow networks, high latency, and complete disconnection.
- Browser DevTools: In Chrome, go to "More Tools" > "Developer Tools" > "Network" tab. You can select pre-defined throttling profiles or create custom ones.
- iOS Simulator: "Network Link Conditioner" can be enabled via "Develop" > "Network Link Conditioner" in the menu bar.
- Android Emulator: In the emulator's extended controls, navigate to "Cellular" and select different network types or "Offline."
- Proxy Tools: Tools like Charles Proxy or Fiddler can intercept network traffic and be configured to simulate network failures, latency, or to return specific error responses.
- Backend Mocking/Stubbing: For API-driven applications, mock servers or service virtualization tools can be configured to return specific HTTP status codes (e.g., 404, 500, 503) or malformed responses, effectively simulating server-side errors. This is invaluable for testing backend-induced errors.
##### Manipulating Data and Backend States
- Database Manipulation: For applications with a persistent backend, directly manipulating the database to create scenarios like empty tables, corrupted records, or missing foreign keys can be a powerful testing method. This is typically done by developers or specialized QA engineers with backend access.
- API Interception and Modification: Using proxy tools (as mentioned above) or custom middleware, you can intercept API requests and responses. This allows you to:
- Return empty arrays for data endpoints.
- Return error payloads.
- Modify response times to test loading indicators.
- Simulate partial data responses.
- User Account States: For features tied to user accounts, creating specific user accounts with predefined states (e.g., a new account with no orders, an account with a suspended payment method) can be a straightforward way to test various scenarios.
##### Client-Side State Manipulation
- Clearing Local Storage/Cache: For web applications, clearing browser local storage, session storage, or cookies can simulate a "first-run" experience or a state where cached data is no longer valid.
- App Data Clearing (Mobile): On Android and iOS, users can clear an app's data. For testing, you can simulate this by uninstalling and reinstalling the app or by using development tools to clear app-specific storage.
- Feature Flags: If your application uses feature flags, toggling them off can simulate scenarios where a feature is unavailable or in a degraded state.
#### Manual Testing Techniques
Manual exploration is crucial for discovering nuanced issues and validating the user's journey through error and empty states.
- Exploratory Testing: This involves a more freeform approach where testers creatively explore the application, trying to break it. Testers can actively try to trigger error conditions by:
- Rapidly tapping buttons.
- Entering invalid data in forms.
- Disconnecting the network at critical moments.
- Navigating away from screens mid-operation.
- Persona-Based Testing: Assigning specific user personas to testers (or using automated personas) can uncover issues related to how different user types would experience these states. For example:
- Impatient User: Rapidly taps buttons, navigates away and back, expecting instant results. This can expose race conditions or unhandled state transitions.
- Adversarial User: Actively tries to break the system, inputting unexpected characters, extremely long strings, or attempting forbidden operations.
- Novice User: Might not understand prompts or error messages, highlighting the need for clear, simple language.
- Accessibility User: Might rely on screen readers or keyboard navigation, exposing issues with how error messages are announced or how focus is managed during errors.
#### Automated Testing Approaches
Automation is essential for ensuring consistency and for regression testing these states over time.
- Unit and Integration Tests: These tests can mock dependencies and directly test the logic for handling specific error codes or empty data sets. For example, a unit test might verify that a component renders the correct message when passed an empty array.
- End-to-End (E2E) Tests:
- Network Throttling/Mocking in E2E: Many E2E testing frameworks (like Playwright, Cypress, or Selenium with appropriate WebDriver capabilities) can be configured to intercept network requests and mock responses. This allows you to simulate API errors or empty data directly within your automated tests.
- Command Injection (for mobile): For mobile E2E tests, tools might allow you to simulate network conditions or trigger specific device states.
- Using SUSA for Autonomous Discovery: Platforms like SUSA (SUSATest) excel here. By uploading an APK or pointing it at a web URL, SUSA autonomously explores the application. Its diverse user personas, including those designed to be adversarial or exploratory, will naturally hit failure paths and uncover unhandled error and empty states. SUSA can be configured to track specific user flows (like login or checkout) and will report PASS/FAIL verdicts based on whether these flows are completed successfully or get stuck in an error/empty state. This autonomous discovery complements traditional scripted E2E tests by finding states you might not have explicitly considered.
- Automated Regression Scripts: Once error and empty states are identified and fixed, it's crucial to automate their verification.
- Record and Replay (with caution): While simple record-and-replay can capture some basic states, it's often brittle. A network error that occurs randomly might not be reproducible by simply replaying a recording.
- Scripted Assertions: The most robust approach is to write scripts that *intentionally trigger* these states (e.g., by mocking a network response) and then assert that the correct UI elements, messages, and behaviors are present.
- SUSA's Regression Script Generation: A powerful feature of platforms like SUSA is their ability to automatically generate regression scripts (e.g., Appium for Android, Playwright for Web) from the flows and states they discover. This means that after SUSA finds an error state, you can leverage its output to create automated regression tests that specifically target that discovered failure path in future runs.
Designing Helpful Error Messages and Empty State Guidance
The content displayed during error and empty states is as critical as the technical handling.
#### Error Messaging: Guiding the User, Not Blaming Them
Raw stack traces, cryptic error codes, or messages like "An error occurred" are unacceptable in a user-facing application.
- Be Clear and Concise: State what happened in simple terms. Avoid technical jargon.
- *Bad:*
ERR_CONNECTION_REFUSED: Code 0x80070002 - *Good:* "We can't connect to the server right now. Please check your internet connection."
- Explain the Impact: Briefly inform the user what this means for them (e.g., "Your changes could not be saved").
- Provide Actionable Steps: Tell the user what they can do next.
- "Try again later."
- "Check your internet connection."
- "Retry the operation."
- "Contact support if the problem persists."
- Offer a Retry Mechanism: Whenever possible, include a button or link that allows the user to retry the failed operation. This is especially important for transient network errors.
- Never Reveal Sensitive Information: Error messages should not expose internal system details, database schemas, or file paths that could be exploited by malicious actors.
- Contextualize: Tailor the message to the specific error. A payment failure message should differ from a data loading failure message.
#### Empty State Guidance: Encouraging Engagement
Empty states are opportunities, not failures. They should guide the user on how to populate the screen.
- Welcome and Explain: For first-run or genuinely empty states, explain the purpose of the screen.
- "Welcome to your new dashboard! Add widgets to see your most important metrics."
- Call to Action: Provide a clear, prominent call to action to help the user create content or data.
- "No messages yet. Tap the '+' button to send your first message!"
- "Start building your project by clicking 'New Project'."
- Visual Cues: Use icons or illustrations to make the empty state more engaging and less stark.
- Suggestions or Examples: Offer hints, tips, or examples of what the user could do.
- "Your wishlist is empty. Browse our popular categories like 'Electronics' or 'Home Goods'."
- Differentiate from Errors: Ensure the empty state doesn't look like an error. Users should understand that the absence of data is expected at this stage.
Edge Cases and Production Realities
Some of the most insidious bugs related to error and empty states only manifest in production.
- Intermittent Network Issues: While simulators can create stable "offline" states, real-world networks can be flaky—dropping for a second, then recovering, or experiencing high latency unpredictably. This can lead to race conditions where the UI tries to update based on stale data after a connection is re-established, or where a retry is initiated just as the original request is about to succeed.
- Thundering Herd Problem: If a critical backend service fails, and then recovers, many clients might simultaneously attempt to reconnect, overwhelming the service again. Testing this requires simulating many clients failing and retrying in unison.
- Partial Data Received: An API might return a response where some fields are present and others are missing or malformed, due to issues on the server or during transmission. The client must be robust enough to handle this gracefully, perhaps by displaying placeholders for missing fields or indicating that some data is unavailable.
- State Transitions During Network Loss: Imagine a user filling out a multi-step form. They lose network connectivity *between* steps. If the application doesn't properly save the intermediate state or inform the user, they might lose all their progress.
- Aborted Operations: A user might initiate a long-running operation (like uploading a large file) and then navigate away or close the app. The application needs to handle this gracefully, informing the user about the status or offering to resume.
- Accessibility in Error States: Screen readers need to announce error messages clearly. Keyboard navigation must be managed such that focus moves to the error message or the actionable element when an error occurs. These are often missed in development and require specific accessibility testing.
Checklist for Testing Error and Empty States
Here’s a concise checklist to help ensure thorough testing:
General:
- [ ] Is every "empty state" clearly distinguishable from an "error state"?
- [ ] Are user-facing messages clear, concise, and free of jargon?
- [ ] Do error messages provide actionable steps or retry mechanisms?
- [ ] Are sensitive system details ever exposed in error messages?
- [ ] Is focus management correct for accessibility during error states?
- [ ] Are loading states (spinners, skeletons) informative and not overly prolonged?
Empty States:
- [ ] Is there a distinct "first-run" experience for new users?
- [ ] Is there guidance or a call to action to populate the empty state?
- [ ] Does the empty state look intentional, not like a bug?
Error States:
- [ ] Can network errors (offline, timeout, slow connection) be simulated and handled gracefully?
- [ ] Can server errors (5xx, invalid responses) be simulated and handled gracefully?
- [ ] Can client-side errors (invalid input, unexpected states) be triggered and handled?
- [ ] Is there a clear retry mechanism for transient errors?
- [ ] Does the application recover or provide a clear path forward after an error?
- [ ] Are partial data scenarios handled without confusing the user?
Automation:
- [ ] Are key error and empty states covered by automated tests?
- [ ] Can these states be reliably triggered in automated test environments (e.g., via mocking)?
- [ ] Do automated tests verify the correct UI, messages, and behavior?
Conclusion: Embracing the Imperfect
The true measure of an application's quality often lies not in how perfectly it performs under ideal conditions, but in how gracefully it handles adversity. Error states and empty states are inevitable aspects of software. By adopting a proactive and comprehensive testing strategy that embraces these non-happy paths, we can build more resilient, user-friendly, and trustworthy applications.
This involves understanding the nuances of different states, employing deterministic simulation techniques, crafting helpful user-facing messages, and leveraging both manual and automated testing approaches. Platforms like SUSA provide powerful capabilities for autonomous discovery of these states and can even generate regression tests, ensuring that your application remains robust against failures.
Investing time in testing error and empty states is not just about finding bugs; it’s about engineering for resilience, building user confidence, and ultimately delivering a superior product. It's about moving beyond the happy path to ensure a positive experience, no matter what the system throws at the user.
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free