Functions

Try

Safe exception handling with tryCatch

The Try utilities provide safe exception handling by converting code that might throw into Result types. This allows you to handle errors explicitly without try-catch blocks.

Why Use Try?

JavaScript's try-catch has several problems:

  • Type safety: You can't specify what errors might be thrown in the type signature
  • Hidden control flow: Exceptions create invisible goto statements
  • Forced handling: With Result, errors must be handled explicitly

Try solves these by catching exceptions and converting them to Result or AsyncResult.

Synchronous: tryCatch()

Wrap synchronous code that might throw:

import { tryCatch } from '@deessejs/functions';

const result = tryCatch(() => {
  return JSON.parse(invalidJson);
});

// Result is Failure if JSON.parse throws
if (result.isFailure()) {
  console.error("Parse error:", result.error);
} else {
  console.log("Parsed:", result.value);
}

Real-World Example

function parseConfig(jsonString: string): Result<Config, Error> {
  return tryCatch(() => {
    const data = JSON.parse(jsonString);

    // Zod validation
    return configSchema.parse(data);
  });
}

// Usage
const result = parseConfig(configJson);

result.match({
  onSuccess: (config) => {
    console.log("Config loaded:", config);
  },
  onFailure: (error) => {
    console.error("Invalid config:", error.message);
  },
});

Asynchronous: tryCatchAsync()

Wrap async code that might throw:

import { tryCatchAsync } from '@deessejs/functions';

const result = await tryCatchAsync(async () => {
  const response = await fetch("/api/data");
  return await response.json();
});

// Result is Failure if fetch or JSON.parse throws
if (result.isFailure()) {
  console.error("Fetch error:", result.error);
} else {
  console.log("Data:", result.value);
}

Real-World Example

async function fetchUser(id: string): AsyncResult<User, Error> {
  return tryCatchAsync(async () => {
    const response = await fetch(`/api/users/${id}`);

    if (!response.ok) {
      throw new Error(`User not found: ${id}`);
    }

    const data = await response.json();

    // Validate with Zod
    return userSchema.parse(data);
  });
}

// Usage
const result = await fetchUser("123");

result.match({
  onSuccess: (user) => {
    console.log("User:", user.name);
  },
  onFailure: (error) => {
    console.error("Error:", error.message);
  },
});

Error Handling

Typed Errors

Specify the error type for better type safety:

import { tryCatch } from '@deessejs/functions';

type ParseError = SyntaxError;

const result = tryCatch<string, ParseError>(() => {
  return JSON.parse(invalidString);
});

// TypeScript knows result.error is ParseError
if (result.isFailure()) {
  console.error("Parse failed:", result.error.message);
}

Custom Error Types

type ValidationError = { field: string; message: string };

function validateUser(data: unknown): Result<User, ValidationError> {
  return tryCatch(() => {
    // This might throw a ValidationError
    return userSchema.parse(data);
  });
}

Type Signatures

// Synchronous
function tryCatch<T, E = unknown>(fn: () => T): Result<T, E>

// Asynchronous
function tryCatchAsync<T, E = unknown>(
  fn: () => Promise<T> | T
): AsyncResult<T, E>

Common Patterns

Safe JSON Parsing

function safeParse<T>(json: string): Result<T, Error> {
  return tryCatch(() => JSON.parse(json));
}

Safe File Reading

import { promises as fs } from 'fs';

async function readFileSafe(path: string): AsyncResult<string, Error> {
  return tryCatchAsync(async () => {
    return await fs.readFile(path, 'utf-8');
  });
}

Safe API Calls

async function fetchAPI<T>(url: string): AsyncResult<T, Error> {
  return tryCatchAsync(async () => {
    const response = await fetch(url);

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    return await response.json() as T;
  });
}

Safe Object Access

function getProperty<T extends object, K extends keyof T>(
  obj: T,
  key: K
): Result<T[K], Error> {
  return tryCatch(() => {
    if (!(key in obj)) {
      throw new Error(`Property '${String(key)}' not found`);
    }
    return obj[key];
  });
}

Best Practices

1. Use Try for External Code

// ✅ Good - wrap external/lib code
const result = tryCatch(() => JSON.parse(jsonString));

// ❌ Unnecessary - your own code should return Result
const result = tryCatch(() => {
  if (invalid) {
    throw new Error("Invalid"); // Don't do this
  }
  return success(value);
});

2. Handle Errors Appropriately

// ✅ Good - explicit error handling
const result = tryCatch(() => riskyOperation());

result.match({
  onSuccess: (value) => console.log("Success:", value),
  onFailure: (error) => console.error("Error:", error.message),
});

// ❌ Bad - ignoring errors
const result = tryCatch(() => riskyOperation());
// What if it fails?

3. Validate After Try

// ✅ Good - combine Try with validation
function parseAndValidate(json: string): Result<User, Error> {
  const parsed = tryCatch(() => JSON.parse(json));

  return parsed.match({
    onSuccess: (data) => {
      // Validate with Zod
      const result = userSchema.safeParse(data);
      return result.success
        ? success(result.data)
        : failure(new Error(result.error.message));
    },
    onFailure: (error) => failure(error),
  });
}

4. Use Specific Error Types

// ✅ Good - specific error type
function readFile(path: string): AsyncResult<string, NodeJS.ErrnoException> {
  return tryCatchAsync(async () => {
    return await fs.readFile(path, 'utf-8');
  });
}

// ⚠️ Less clear - generic error
function readFile(path: string): AsyncResult<string, Error> {
  return tryCatchAsync(async () => {
    return await fs.readFile(path, 'utf-8');
  });
}

When NOT to Use Try

Don't Use for Expected Failures

// ❌ Bad - using try-catch for business logic
function getUser(id: string): Result<User, Error> {
  return tryCatch(() => {
    const user = db.find(id);

    if (!user) {
      throw new Error("User not found"); // This is business logic!
    }

    return user;
  });
}

// ✅ Good - explicit Result
function getUser(id: string): Result<User, Error> {
  const user = db.find(id);

  if (!user) {
    return failure(new Error("User not found"));
  }

  return success(user);
}

Don't Nest Try

// ❌ Bad - nested tryCatch
const result = tryCatch(() => {
  const inner = tryCatch(() => risky1());
  // ...
});

// ✅ Good - flat structure
const result1 = tryCatch(() => risky1());
if (result1.isFailure()) {
  return result1;
}

const result2 = tryCatch(() => risky2(result1.value));

Converting Between Types

Try to Result

tryCatch and tryCatchAsync already return Result and AsyncResult - no conversion needed!

Exception to Outcome

import { tryCatchAsync, exceptionOutcome, exceptionFromError } from '@deessejs/functions';

async function fetchWithOutcome(id: string): Outcome<User, never, Exception> {
  try {
    const user = await fetchUser(id);
    return successOutcome(user);
  } catch (error) {
    return exceptionOutcome(exceptionFromError(error as Error));
  }
}

See Also

  • Result - The success/failure type returned by tryCatch
  • Async Result - Async version returned by tryCatchAsync
  • Outcome - Advanced system distinguishing causes from exceptions

On this page