Intermediate 40 min.NET 10

Errors, empty states & recovery

Design failure paths that help users recover and give developers enough evidence to diagnose the cause.

BY THE END OF THIS TUTORIAL

Here's what you'll be able to do.

  • Separate validation failures, missing resources, and exceptions
  • Return useful empty states
  • Use a safe production error page with a request identifier
Before you begin

Understand form validation and status codes. Inspect HomeController.Error and the task list view.

Understand the idea

Not every unsuccessful operation is an exception. An invalid title is a validation result. A task that does not exist is a missing resource. A database connection failure is an operational error. Treating all three as “something went wrong” makes the product less usable and the logs less informative.

The best recovery path depends on what the user can do. A form can explain how to correct input. An empty list can invite a first task. An unexpected server failure should preserve privacy while providing an identifier for support.

Choose your environment

Open TaskBoard.csproj from the example download. Install the ASP.NET and web development workload and a Visual Studio release supporting .NET 10. Select the TaskBoard launch profile and press F5. Use Solution Explorer to find the files named below.

Download the complete companion project
STEP 01

Keep expected failures explicit

TaskBoard returns the same form for invalid ModelState and returns NotFound for an absent owned row. These branches are ordinary application behavior. They do not need exception stack traces or alarming infrastructure alerts.

Avoid catching every exception and returning a success page. That hides failures from both users and monitoring. If you catch an operational exception, do so because you can add context, recover, or translate it into a meaningful result.

Controller excerpt
if (task is null) return NotFound();
if (!ModelState.IsValid) return View(input);

// Only the valid, authorized branch mutates the entity.
task.Title = input.Title.Trim();
await db.SaveChangesAsync(ct);
STEP 02

Make absence useful

A search with no matches is usually a successful 200 response with an empty collection. Show what happened and offer a next step: change the query or create a task. Keep filters visible so the user understands why the list is empty.

Do not display fabricated records just to fill a layout. Empty states are a normal part of a real application, especially immediately after registration. They deserve the same care as populated screens.

Razor excerpt
@if (Model.Tasks.Count == 0)
{
    <section>
        <h2>No tasks found</h2>
        <p>Create your first task or try another search.</p>
    </section>
}
STEP 03

Use a safe production exception page

The production pipeline re-executes /Home/Error when an unhandled exception occurs. The view receives a request identifier, not the exception object. This prevents stack traces, file paths, and connection details from being exposed to every visitor.

The action disables response caching. Error responses should not become a shared cached representation of a transient failure. Keep the error endpoint itself lightweight so it can render even when the database is unavailable.

Controllers/HomeController.cs
using Microsoft.AspNetCore.Mvc;

namespace TaskBoard.Controllers;

public class HomeController : Controller
{
    public IActionResult Index() => View();
    [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
    public IActionResult Error() => View("Error", HttpContext.TraceIdentifier);
}
STEP 04

Test recovery rather than appearance alone

Try invalid input, an impossible task id, an empty search, and an unavailable database in an isolated development copy. For a production error-page check, run a controlled test endpoint only in a local test fixture; do not leave a public “throw exception” route in the application.

Verify the status code as well as the text. A friendly page that returns 200 for an actual server failure can mislead monitoring and clients. Review logs to ensure the request can be traced without exposing private form content.

Views/Home/Error.cshtml
@model string
@{ ViewData["Title"] = "Something went wrong"; }
<h1>We couldn't finish that request.</h1><p>Try again. If it keeps happening, use request ID <code>@Model</code> to find the corresponding server log.</p><a href="/">Return home</a>
YOUR TURN

Make it your own.

Write down the expected status and user-facing behavior for an empty search, a missing task, an invalid form, and an unavailable database.

See the solution & reasoning

An empty search returns 200 with an empty state. A missing owned task returns 404. An invalid MVC form is redisplayed with validation errors, normally as 200, and no write occurs. An unexpected database failure should produce a server error and a safe error page in production, with diagnostic details restricted to logs.

When something doesn't work

A database-backed error page can fail for the same reason as the original request. Keep it independent of fragile services.

If you see developer exception details publicly, check the effective environment and hosting configuration.

Retrying a write blindly can duplicate work. For operations where retries are necessary, design idempotency rather than simply repeating the POST.

CHECK YOUR UNDERSTANDING

Should an empty search result normally be treated as a server error?

Should an empty search result normally be treated as a server error?

ONE STEP FURTHER

That's another skill in your toolkit.

Your progress is saved in this browser. Sign in to save completion to your account.