Advanced TypeScript Error Handling with neverthrow: An E-commerce Example

Enhance your TypeScript projects with robust, type-safe error handling using neverthrow. This guide covers Rust-like error patterns in an e-commerce context, addressing common issues effectively. Boost your coding efficiency with expert tips.

Advanced TypeScript Error Handling with neverthrow: An E-commerce Example
Photo by Brett Jordan / Unsplash

Advanced TypeScript Error Handling with neverthrow: An E-commerce Example

In this tutorial, we'll explore how to handle errors in a TypeScript application using the neverthrow library, focusing on a practical e-commerce scenario. Specifically, we'll write a service to check product availability and place an order, handling potential errors in a type-safe manner similar to Rust's error handling.

Initial Setup

Let's set up our environment with TypeScript and the necessary domain-driven design elements. First, we'll define the models and interfaces that represent the domain model, and import the necessary packages like neverthrow and luxon for date handling.

Importing Required Libraries

import { Product } from '@domain/product/Product';
import { InventoryRepositoryInterface } from '@domain/inventory/InventoryRepositoryInterface';
import { Order, OrderDetails } from '@domain/order/Order';
import { OrderRepositoryInterface } from '@domain/order/OrderRepositoryInterface';
import { DateTime } from 'luxon';
import { err, ok, Result } from 'neverthrow';
import { makeTaggedUnion, none, MemberType } from 'safety-match';

Defining Input and Error Types

Next, we define the input for our service and the types of errors that could occur:

export interface PlaceOrderInput {
  productId: string;
  quantity: number;
  orderDate: DateTime;

export const PlaceOrderError = makeTaggedUnion({
  ProductNotFound: none,
  InsufficientStock: none,
  OrderSaveFailed: none,

export type PlaceOrderError = MemberType<typeof PlaceOrderError>;

In this setup, PlaceOrderInput specifies the information required to place an order, and PlaceOrderError defines possible error scenarios.

Implementing the Service

Now, let's implement the service. We'll use the neverthrow library to manage different outcomes of operations explicitly, ensuring that every function call that might fail is clear in its intent and handling.

export class OrderService {
    private readonly inventoryRepository: InventoryRepositoryInterface,
    private readonly orderRepository: OrderRepositoryInterface
  ) {}

  async placeOrder(input: PlaceOrderInput): Promise<Result<Order, PlaceOrderError>> {
    // Check product availability
    const stockLevel = await this.inventoryRepository.getStockLevel(input.productId);
    if (stockLevel === null) return err(PlaceOrderError.ProductNotFound);
    if (stockLevel < input.quantity) return err(PlaceOrderError.InsufficientStock);

    // Create and save the order
    const orderDetails: OrderDetails = {
      productId: input.productId,
      quantity: input.quantity,
      orderDate: input.orderDate
    const order = new Order(orderDetails);
    const saveResult = await this.orderRepository.saveOrder(order);

    if (!saveResult) return err(PlaceOrderError.OrderSaveFailed);
    return ok(order);

Explanation of Service Logic:

  1. Check Product Availability: The service first checks if the product is available and in sufficient quantity. It returns early with an appropriate error if not.
  2. Place the Order: If the product is available, the service creates an order. It attempts to save this order using the order repository.
  3. Error Handling: If saving the order fails, an error is returned. Otherwise, the successfully placed order is returned.

Benefits of This Approach

Using neverthrow provides several advantages:

  • Explicit Error Handling: Each function's outcome, whether success or failure, is clearly defined through its return type.
  • Type Safety: Errors are part of the type system, forcing developers to handle them explicitly, reducing the chance of unhandled exceptions.
  • Improved Maintainability: Having a clear and predictable error handling strategy makes the code easier to maintain and debug.

This method aligns with modern best practices in TypeScript application development, where enhancing clarity, type safety, and robustness are key priorities. By structuring your error handling in this detailed manner, your codebase becomes more predictable and less prone to runtime errors.
To conclude, while this tutorial showcases an approach to structured and type-safe error handling in TypeScript using the neverthrow library, I must credit the initial inspiration to my colleague, Killian. His insights and innovative ideas significantly influenced the development of these error handling techniques, demonstrating the power of collaboration in software development.