receta
Result

Constructors

Creating Results

How to create Result values from successful operations, failures, and risky code.

Overview

FunctionPurposeReal-World Analogy
ok(value)Wrap a successful valueHTTP 200 OK response
err(error)Wrap a failure valueHTTP 400/500 error response
tryCatch(fn)Catch exceptions as errorstry-catch block as a value
tryCatchAsync(fn)Catch async exceptionstry-catch for Promises
fromNullable(value, error)Convert null/undefined to errorDatabase query returning 0 rows

ok(value) - Success Response

When to use: You have a value and want to wrap it in a Result.

Real-World: Stripe Payment Success

import { ok } from '@ontologyio/receta/result'

// Stripe returns payment confirmation
const createCharge = (token: string, amount: number) => {
  const charge = {
    id: 'ch_abc123',
    amount: amount,
    status: 'succeeded',
    created: Date.now()
  }

  return ok(charge) // ✅ Explicit success
}

// Type: Ok<{ id: string, amount: number, ... }>
// "This function always succeeds"

Real-World: GitHub API Success

// When repo exists
const fetchRepo = async (owner: string, name: string) => {
  const res = await fetch(`https://api.github.com/repos/${owner}/${name}`)
  const repo = await res.json()
  return ok({
    name: repo.name,
    stars: repo.stargazers_count,
    language: repo.language
  })
}

Pattern: Use ok() when you're certain the operation succeeded and want to return a Result for consistency with other functions.


err(error) - Explicit Failure

When to use: You know something failed and want to communicate the error clearly.

Real-World: Stripe Card Declined

import { err } from '@ontologyio/receta/result'

type CardError = {
  type: 'card_error'
  code: 'card_declined' | 'insufficient_funds' | 'expired_card'
  message: string
}

const validateCard = (cardNumber: string): Result<string, CardError> => {
  if (cardNumber.length !== 16) {
    return err({
      type: 'card_error',
      code: 'card_declined',
      message: 'Card number must be 16 digits'
    })
  }

  return ok(cardNumber)
}

Real-World: AWS S3 Access Denied

type S3Error = {
  code: 'AccessDenied' | 'NoSuchBucket' | 'NoSuchKey'
  message: string
  resource: string
}

const getObject = (bucket: string, key: string): Result<Buffer, S3Error> => {
  if (!hasPermission(bucket)) {
    return err({
      code: 'AccessDenied',
      message: 'You do not have permission to access this bucket',
      resource: `${bucket}/${key}`
    })
  }

  return ok(readObject(bucket, key))
}

Real-World: Form Validation Errors

const validateEmail = (email: string) => {
  if (!email.includes('@')) {
    return err('Email must contain @')
  }
  if (email.length < 5) {
    return err('Email too short')
  }
  return ok(email)
}

Pattern: Use err() for business logic failures - when you can predict and describe what went wrong.


tryCatch(fn) - Safe Operations

When to use: Code that might throw exceptions (like JSON.parse, new URL, etc.)

Real-World: Parsing API Responses

import { tryCatch } from '@ontologyio/receta/result'

// GitHub API returns JSON - but what if it's malformed?
const parseGitHubResponse = (body: string) =>
  tryCatch(() => JSON.parse(body))

// Type: Result<any, unknown>
parseGitHubResponse('{"login":"octocat"}') // Ok({ login: 'octocat' })
parseGitHubResponse('invalid json')         // Err(SyntaxError)

Real-World: URL Parsing (Next.js, React Router)

const parseUrl = (url: string) =>
  tryCatch(() => new URL(url))

parseUrl('https://example.com')  // Ok(URL)
parseUrl('not a url')            // Err(TypeError)

Custom Error Mapping

// Map raw errors to your domain errors
type ParseError = {
  type: 'parse_error'
  input: string
  reason: string
}

const parseJSON = <T>(str: string): Result<T, ParseError> =>
  tryCatch(
    () => JSON.parse(str) as T,
    (error) => ({
      type: 'parse_error',
      input: str.slice(0, 100), // First 100 chars
      reason: String(error)
    })
  )

Pattern: Use tryCatch to convert exceptions to values. Map the error to match your domain language.


tryCatchAsync(fn) - Async Safety

When to use: Async operations that might fail (fetch, database calls, file I/O).

Real-World: Stripe API Calls

import { tryCatchAsync } from '@ontologyio/receta/result'

type StripeError = {
  type: 'api_error' | 'network_error'
  message: string
}

const createCharge = async (
  token: string,
  amount: number
): Promise<Result<Charge, StripeError>> =>
  tryCatchAsync(
    async () => {
      const charge = await stripe.charges.create({
        amount,
        currency: 'usd',
        source: token
      })
      return charge
    },
    (error) => ({
      type: 'api_error',
      message: String(error)
    })
  )

// Usage
const result = await createCharge('tok_visa', 1000)
match(result, {
  onOk: (charge) => console.log('Charged:', charge.id),
  onErr: (error) => console.error('Failed:', error.message)
})

Real-World: Prisma Queries

// Prisma rejects on constraint violations
const createUser = (email: string) =>
  tryCatchAsync(
    () => db.user.create({ data: { email } }),
    (error: any) => {
      if (error.code === 'P2002') {
        return { type: 'duplicate_email', email }
      }
      return { type: 'database_error', error }
    }
  )

Real-World: Supabase/Database Queries

const fetchUser = async (id: string) =>
  tryCatchAsync(
    async () => {
      const { data, error } = await supabase
        .from('users')
        .select('*')
        .eq('id', id)
        .single()

      if (error) throw error
      return data
    },
    (error) => ({
      code: 'DB_ERROR',
      message: String(error)
    })
  )

Real-World: File System Operations

import fs from 'fs/promises'

const readConfig = async (path: string) =>
  tryCatchAsync(
    async () => {
      const content = await fs.readFile(path, 'utf8')
      return JSON.parse(content)
    },
    (error: any) => {
      if (error.code === 'ENOENT') {
        return { type: 'not_found', path }
      }
      if (error instanceof SyntaxError) {
        return { type: 'invalid_json', path }
      }
      return { type: 'unknown', error: String(error) }
    }
  )

Real-World: Fetch with Timeout

const fetchWithTimeout = async (url: string, timeout: number) =>
  tryCatchAsync(
    async () => {
      const controller = new AbortController()
      const id = setTimeout(() => controller.abort(), timeout)

      try {
        const res = await fetch(url, { signal: controller.signal })
        return res.json()
      } finally {
        clearTimeout(id)
      }
    },
    (error: any) => {
      if (error.name === 'AbortError') {
        return { type: 'timeout', url, timeout }
      }
      return { type: 'network', message: error.message }
    }
  )

Pattern: Use tryCatchAsync for any async operation. Map errors to domain-specific types.


fromNullable(value, error) - Handle Missing Data

When to use: Database queries, Array.find(), optional config values, dictionary lookups.

Real-World: Database Queries

import { fromNullable } from '@ontologyio/receta/result'

// Prisma findFirst returns null if not found
const findUserByEmail = async (email: string) => {
  const user = await db.user.findFirst({ where: { email } })

  return fromNullable(user, {
    type: 'not_found',
    resource: 'user',
    query: { email }
  })
}

// Type: Result<User, { type: 'not_found', ... }>

Real-World: Array.find() - Finding Products

const products = [
  { id: '1', name: 'iPhone', price: 999 },
  { id: '2', name: 'MacBook', price: 1999 }
]

const findProduct = (id: string) =>
  fromNullable(
    products.find(p => p.id === id),
    { type: 'product_not_found', id }
  )

findProduct('1') // Ok({ id: '1', name: 'iPhone', ... })
findProduct('99') // Err({ type: 'product_not_found', id: '99' })

Real-World: Config/Environment Variables

// Next.js / Vite environment variables
const getEnvVar = (key: string) =>
  fromNullable(
    process.env[key],
    { type: 'missing_env_var', key }
  )

const config = pipe(
  getEnvVar('DATABASE_URL'),
  flatMap(url => map(getEnvVar('API_KEY'), key => ({ url, key }))),
  unwrapOr({ url: 'default', key: 'default' })
)

Real-World: URL Query Parameters

// Express.js / Next.js API route
const getUserIdFromQuery = (query: Record<string, string | undefined>) =>
  fromNullable(
    query.userId,
    { type: 'missing_param', param: 'userId' }
  )

// app.get('/api/users', (req, res) => {
//   const result = getUserIdFromQuery(req.query)
//   ...
// })

Real-World: localStorage (Browser)

const getStoredUser = () =>
  pipe(
    fromNullable(localStorage.getItem('user'), 'No user in storage'),
    flatMap(str => tryCatch(() => JSON.parse(str))),
    mapErr(() => 'Invalid user data in storage')
  )

Pattern: Use fromNullable when an operation might return nothing, and you want to treat "nothing" as an error.


Comparison Table

ScenarioConstructorExample
Always succeedsok(value)Formatting a string
Known failureerr(error)Validation fails
Might throwtryCatch(fn)JSON.parse()
Async might throwtryCatchAsync(fn)fetch(), database
Might be nullfromNullable(value, error)Array.find(), ?.

Common Patterns

1. Validation Chain

const validateSignup = (email: string, password: string) =>
  pipe(
    validateEmail(email),           // Result<string, string>
    flatMap(() => validatePassword(password)), // Chains validations
    map(() => ({ email, password }))
  )

2. API Call with Fallback

const fetchUserWithFallback = async (id: string) => {
  const primary = await tryCatchAsync(() => fetchFromPrimary(id))

  return isOk(primary) ? primary : tryCatchAsync(() => fetchFromCache(id))
}

3. Required Config

const loadConfig = () => {
  const dbUrl = fromNullable(process.env.DATABASE_URL, 'Missing DATABASE_URL')
  const apiKey = fromNullable(process.env.API_KEY, 'Missing API_KEY')

  return pipe(
    collect([dbUrl, apiKey]),
    map(([url, key]) => ({ databaseUrl: url, apiKey: key }))
  )
}

Next Steps

  • Transformers: Work with Results using map, flatMap, etc.
  • Extractors: Get values out with unwrap, match
  • Patterns: Complete recipes for common scenarios

On this page