fullstackhero/dotnet-starter-kit · error · NotFoundException

Product not found.

Error message

Product {query.ProductId} not found.

What it means

GetProductByIdQueryHandler runs an AsNoTracking FirstOrDefault for the given ProductId and throws NotFoundException when no product matches, producing HTTP 404. Because the query uses the default DbContext filters, soft-deleted products and other tenants' products are invisible.

Solutions

  1. Validate the id against the products list endpoint before deep-linking, or handle 404 gracefully in the UI with a 'product not available' state.
  2. If the product should exist but was deleted, call the restore endpoint (requires appropriate permissions).
  3. Check tenant context/headers — the product may exist under a different tenant.
  4. Verify environment: dev ids rarely exist in prod and vice versa.

Example fix

// before
const product = await api.getProduct(id); // throws 404, crashes page
// after
const product = await api.getProduct(id).catch(e =>
  e.status === 404 ? null : Promise.reject(e)
);
if (!product) return <NotFound message="Product not available" />;
Defensive patterns

Strategy: try-catch

Validate before calling

// validate GUID format before calling
const isGuid = (v) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v);
if (!isGuid(productId)) throw new Error('Invalid product id');

Type guard

function isProductDto(x): x is ProductDto {
  return !!x && typeof x === 'object' && typeof x.id === 'string' && typeof x.name === 'string';
}

Try / catch

try {
  const product = await api.getProduct(productId);
} catch (e) {
  if (e.status === 404) return <NotFoundPage />;
  throw e;
}

Prevention

When it happens

Trigger: GET /products/{id} with a nonexistent id, a soft-deleted product's id, a wrong-tenant product id, or an id from another environment's database.

Common situations: Bookmarked/shared links to products deleted since; navigation after delete without list refresh; tenant switch on the client while old ids remain in state.

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15). Data as JSON: /api/errors/176e2b8de3ebe1b1. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Catalog/Modules.Catalog/Features/v1/Products/GetProductById/GetProductByIdQueryHandler.cs:21

using FSH.Modules.Catalog.Contracts.v1.Products;
using FSH.Modules.Catalog.Data;
using Mediator;
using Microsoft.EntityFrameworkCore;

namespace FSH.Modules.Catalog.Features.v1.Products.GetProductById;

public sealed class GetProductByIdQueryHandler(CatalogDbContext dbContext)
    : IQueryHandler<GetProductByIdQuery, ProductDto>
{
    public async ValueTask<ProductDto> Handle(GetProductByIdQuery query, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(query);

        var product = await dbContext.Products
            .AsNoTracking()
            .FirstOrDefaultAsync(p => p.Id == query.ProductId, cancellationToken)
            .ConfigureAwait(false)
            ?? throw new NotFoundException($"Product {query.ProductId} not found.");

        return product.ToDto();
    }
}

View on GitHub (pinned to 3f2959e683)