fullstackhero/dotnet-starter-kit · error · CustomException

A product with SKU ' ' already exists.

Error message

A product with SKU '{product.Sku}' already exists.

What it means

CreateProductCommandHandler checks whether any Product already uses the same SKU before inserting. If taken, it throws CustomException with HttpStatusCode.Conflict, producing HTTP 409. SKU is a unique business key, so duplicates are rejected explicitly rather than by a DB unique constraint.

Solutions

  1. Check SKU availability first (list/search products by SKU) and use a different SKU.
  2. If this is a retry, GET the product by SKU — the original create likely succeeded; reuse the existing product instead of re-posting.
  3. Add client-side idempotency: disable the submit button while the request is in flight.
  4. If a unique constraint violation surfaces instead, it means the app-level check raced — catch 409/unique-violation and surface a friendly message.

Example fix

// before
await api.createProduct({ sku: form.sku, name: form.name });
// after
const existing = await api.searchProducts({ sku: form.sku });
if (existing.items.length > 0) {
  throw new Error(`SKU ${form.sku} is already in use`);
}
await api.createProduct({ sku: form.sku, name: form.name });
Defensive patterns

Strategy: validation

Validate before calling

const dup = await api.searchProducts({ sku });
if (dup.items.length > 0) throw new Error(`SKU ${sku} already in use`);

Try / catch

try {
  await api.createProduct(payload);
} catch (e) {
  if (e.status === 409) {
    showError(`SKU ${payload.sku} already exists`);
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: POST /products (v1) whose derived Product.Sku equals the Sku of any existing product row — duplicate form submission, retry of a request that actually succeeded, or importing a catalog that already contains the SKU.

Common situations: Double-clicking 'Create' or replaying a POST after a timeout; CSV/bulk import colliding with existing inventory; two environments sharing one database.

Related errors


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

Appendix: source

Thrown at src/Modules/Catalog/Modules.Catalog/Features/v1/Products/CreateProduct/CreateProductCommandHandler.cs:49

        {
            throw new NotFoundException($"Category {command.CategoryId} not found.");
        }

        var product = Product.Create(
            command.Sku,
            command.Name,
            command.Description,
            command.BrandId,
            command.CategoryId,
            new Money(command.PriceAmount, command.PriceCurrency),
            command.Stock);

        bool skuTaken = await dbContext.Products
            .AnyAsync(p => p.Sku == product.Sku, cancellationToken)
            .ConfigureAwait(false);
        if (skuTaken)
        {
            throw new CustomException(
                $"A product with SKU '{product.Sku}' already exists.",
                (IEnumerable<string>?)null,
                HttpStatusCode.Conflict);
        }

        bool slugTaken = await dbContext.Products
            .AnyAsync(p => p.Slug == product.Slug, cancellationToken)
            .ConfigureAwait(false);
        if (slugTaken)
        {
            throw new CustomException(
                $"A product with name '{command.Name}' already exists.",
                (IEnumerable<string>?)null,
                HttpStatusCode.Conflict);
        }

        dbContext.Products.Add(product);
        await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);

View on GitHub (pinned to 3f2959e683)