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
Creates a successful result tuple.
ok<T>(data: T): [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]
Error type: "NotFoundError", "UnauthorizedError", "InternalServerError", "ValidationError", or any custom string
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 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]
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.