Skip to main content

Error Utilities

Result-based error handling inspired by functional programming patterns. Avoid throwing exceptions and handle errors explicitly with type-safe tuples.

Import

import { ok, err, mightThrow, mightThrowSync } from "semola/errors";

Functions

ok

Creates a successful result tuple.
ok<T>(data: T): [null, T]
data
T
required
Success value
result
[null, T]
Result tuple with null error and the data value
Example:
const result = ok({ userId: 123, name: "John" });
// [null, { userId: 123, name: "John" }]

const [error, data] = result;

if (error) {
  // Handle error
} else {
  console.log(data.userId); // Type-safe access
}

err

Creates an error result tuple with a typed error object.
err<T extends CommonError>(type: T, message: string): [{ type: T; message: string }, null]
type
CommonError
required
Error type: "NotFoundError", "UnauthorizedError", "InternalServerError", "ValidationError", or any custom string
message
string
required
Human-readable error message
result
[{ type: T; message: string }, null]
Result tuple with typed error object and null data
Example:
const result = err("NotFoundError", "User not found");
// [{ type: "NotFoundError", message: "User not found" }, null]

const [error, data] = result;

if (error) {
  console.log(error.type);    // "NotFoundError"
  console.log(error.message); // "User not found"
}

mightThrow

Wraps async operations that might throw into result tuples.
mightThrow<T>(promise: Promise<T>): Promise<[unknown, null] | [null, T]>
promise
Promise<T>
required
Promise that might throw an error
result
Promise<[null, T] | [unknown, null]>
Promise resolving to result tuple with either:
  • [null, data] - Success with resolved value
  • [error, null] - Error with thrown value
Example:
const [error, data] = await mightThrow(fetch('/api/users'));

if (error) {
  console.error("Request failed:", error);
  return;
}

console.log("Success:", data);

mightThrowSync

Wraps synchronous operations that might throw into result tuples.
mightThrowSync<T>(fn: () => T): [unknown, null] | [null, T]
fn
() => T
required
Function that might throw an error
result
[null, T] | [unknown, null]
Result tuple with either:
  • [null, data] - Success with return value
  • [error, null] - Error with thrown value
Example:
const [error, data] = mightThrowSync(() => JSON.parse(input));

if (error) {
  console.error("Parse failed:", error);
  return;
}

console.log("Parsed:", data);

Type Definitions

CommonError

type CommonError =
  | "NotFoundError"
  | "UnauthorizedError"
  | "InternalServerError"
  | "ValidationError"
  | (string & {});
Common error type strings. The string & {} allows any custom string while preserving autocomplete for common types.

Usage Examples

Basic Error Handling

import { ok, err } from "semola/errors";

async function getUser(id: string) {
  if (!id) {
    return err("ValidationError", "User ID is required");
  }
  
  const user = await database.findUser(id);
  
  if (!user) {
    return err("NotFoundError", "User not found");
  }
  
  return ok(user);
}

// Usage
const [error, user] = await getUser("123");

if (error) {
  switch (error.type) {
    case "ValidationError":
      console.log("Validation failed:", error.message);
      break;
    case "NotFoundError":
      console.log("User not found");
      break;
    default:
      console.log("Error:", error.message);
  }
} else {
  console.log("User:", user);
}

Wrapping External APIs

import { ok, err, mightThrow } from "semola/errors";

async function fetchUser(id: string) {
  const [fetchError, response] = await mightThrow(
    fetch(`https://api.example.com/users/${id}`)
  );
  
  if (fetchError) {
    return err("InternalServerError", "Failed to fetch user");
  }
  
  if (!response.ok) {
    return err("NotFoundError", "User not found");
  }
  
  const [parseError, user] = await mightThrow(response.json());
  
  if (parseError) {
    return err("InternalServerError", "Invalid response format");
  }
  
  return ok(user);
}

Chaining Operations

import { ok, err, mightThrow } from "semola/errors";

async function createPost(userId: string, title: string, content: string) {
  // Validate user
  const [userError, user] = await getUser(userId);
  if (userError) return err("UnauthorizedError", "Invalid user");
  
  // Validate input
  if (!title || !content) {
    return err("ValidationError", "Title and content are required");
  }
  
  // Create post
  const [dbError, post] = await mightThrow(
    database.createPost({
      userId: user.id,
      title,
      content,
      createdAt: Date.now(),
    })
  );
  
  if (dbError) {
    return err("InternalServerError", "Failed to create post");
  }
  
  return ok(post);
}

// Usage
const [error, post] = await createPost("123", "My Post", "Content...");

if (error) {
  console.error(`${error.type}: ${error.message}`);
} else {
  console.log("Post created:", post.id);
}

Parsing JSON

import { mightThrowSync, err, ok } from "semola/errors";

function parseConfig(json: string) {
  const [parseError, config] = mightThrowSync(() => JSON.parse(json));
  
  if (parseError) {
    return err("ValidationError", "Invalid JSON configuration");
  }
  
  // Validate structure
  if (!config.apiKey || !config.endpoint) {
    return err("ValidationError", "Missing required configuration fields");
  }
  
  return ok(config);
}

const [error, config] = parseConfig(input);

if (error) {
  console.error("Configuration error:", error.message);
  process.exit(1);
}

console.log("Using endpoint:", config.endpoint);

API Route Handler

import { Api } from "semola/api";
import { ok, err } from "semola/errors";
import { z } from "zod";

const api = new Api();

api.defineRoute({
  path: "/users/:id",
  method: "GET",
  request: {
    params: z.object({ id: z.string().uuid() }),
  },
  response: {
    200: z.object({
      id: z.string(),
      name: z.string(),
      email: z.string().email(),
    }),
    404: z.object({ message: z.string() }),
    500: z.object({ message: z.string() }),
  },
  handler: async (c) => {
    const [error, user] = await getUser(c.req.params.id);
    
    if (error) {
      switch (error.type) {
        case "NotFoundError":
          return c.json(404, { message: error.message });
        default:
          return c.json(500, { message: "Internal server error" });
      }
    }
    
    return c.json(200, user);
  },
});

Database Operations

import { mightThrow, ok, err } from "semola/errors";

class UserRepository {
  async create(data: CreateUserInput) {
    const [error, user] = await mightThrow(
      this.db.insert('users', data)
    );
    
    if (error) {
      return err("InternalServerError", "Failed to create user");
    }
    
    return ok(user);
  }
  
  async findById(id: string) {
    const [error, user] = await mightThrow(
      this.db.findOne('users', { id })
    );
    
    if (error) {
      return err("InternalServerError", "Database query failed");
    }
    
    if (!user) {
      return err("NotFoundError", "User not found");
    }
    
    return ok(user);
  }
  
  async update(id: string, data: UpdateUserInput) {
    const [findError, user] = await this.findById(id);
    if (findError) return err(findError.type, findError.message);
    
    const [updateError, updated] = await mightThrow(
      this.db.update('users', { id }, data)
    );
    
    if (updateError) {
      return err("InternalServerError", "Failed to update user");
    }
    
    return ok(updated);
  }
}

File Operations

import { mightThrowSync, ok, err } from "semola/errors";
import * as fs from "fs";

function readConfigFile(path: string) {
  const [readError, content] = mightThrowSync(() =>
    fs.readFileSync(path, 'utf-8')
  );
  
  if (readError) {
    return err("NotFoundError", `Configuration file not found: ${path}`);
  }
  
  const [parseError, config] = mightThrowSync(() => JSON.parse(content));
  
  if (parseError) {
    return err("ValidationError", "Invalid JSON in configuration file");
  }
  
  return ok(config);
}

const [error, config] = readConfigFile('./config.json');

if (error) {
  console.error(error.message);
  process.exit(1);
}

console.log("Loaded config:", config);

Benefits

  • Explicit error handling: Forces you to handle errors at the call site
  • Type-safe: Error types and data types are preserved
  • No try-catch: Cleaner control flow without exception handling
  • Composable: Easy to chain operations and propagate errors
  • Traceable: Clear error paths through your code
  • Functional: Inspired by Result types from Rust, Go, and functional languages

Pattern Comparison

Traditional Try-Catch

try {
  const user = await getUser(id);
  console.log(user);
} catch (error) {
  console.error("Error:", error);
}

Result Pattern

const [error, user] = await getUser(id);

if (error) {
  console.error("Error:", error.message);
} else {
  console.log(user);
}
The result pattern makes error handling explicit and prevents forgotten error checks.

Build docs developers (and LLMs) love