Skip to content

safe()

Execute a Futurable without throwing errors, returning a Result type instead.

Syntax

typescript
futurable.safe<TError = unknown>(): Futurable<SafeResult<T, TError>>

Parameters

TError (optional type parameter)

The type of error expected. Defaults to unknown.

Return Value

A Futurable that resolves to a SafeResult<T, TError> object:

typescript
type SafeResult<T, E = Error> =
  | { success: true; data: T; error: null }
  | { success: false; data: null; error: E };

Description

The safe() method wraps a Futurable in a safe execution context that never throws. Instead of rejecting and requiring try-catch blocks, it always resolves with a result object containing either the data or the error.

This is particularly useful in async/await contexts where you want explicit error handling without wrapping code in try-catch blocks.

Key Benefits

  • No try-catch needed: Errors become values in the success path
  • Type-safe: TypeScript discriminated unions work perfectly
  • Explicit handling: Forces you to handle both success and error cases
  • Cleaner code: Reduces nesting and indentation
  • Composable: Can be chained with other Futurable methods

Examples

Basic Usage

typescript
import { Futurable } from '@ndriadev/futurable';

// Without safe() - requires try-catch
try {
  const data = await Futurable.fetch('/api/data')
    .then(r => r.json());
  console.log('Success:', data);
} catch (error) {
  console.error('Error:', error);
}

// With safe() - no try-catch needed
const result = await Futurable.fetch('/api/data')
  .then(r => r.json())
  .safe();

if (result.success) {
  console.log('Success:', result.data);
} else {
  console.error('Error:', result.error);
}

Type-Safe Error Handling

typescript
// TypeScript knows the exact shape based on success
const result = await Futurable.fetch('/api/users')
  .then(r => r.json())
  .safe();

if (result.success) {
  // TypeScript knows:
  // - result.data exists (type: User[])
  // - result.error is null
  const users = result.data;
  console.log(`Found ${users.length} users`);
} else {
  // TypeScript knows:
  // - result.error exists
  // - result.data is null
  console.error('Failed to fetch users:', result.error);
}

With Custom Error Type

typescript
interface APIError {
  code: string;
  message: string;
  statusCode: number;
}

const result = await Futurable.fetch('/api/data')
  .then(async (response) => {
    if (!response.ok) {
      throw {
        code: 'API_ERROR',
        message: response.statusText,
        statusCode: response.status
      } as APIError;
    }
    return response.json();
  })
  .safe<APIError>();

if (result.success) {
  console.log(result.data);
} else {
  // result.error is typed as APIError
  console.error(`API Error [${result.error.code}]: ${result.error.message}`);
}

Chaining Multiple Operations

typescript
const result = await Futurable.resolve(5)
  .delay(val => val * 2, 1000)
  .then(val => Futurable.fetch(`/api/item/${val}`))
  .then(r => r.json())
  .safe();

if (result.success) {
  processData(result.data);
} else {
  logError(result.error);
}

Early Returns Pattern

typescript
async function fetchUserProfile(userId: number) {
  const result = await Futurable.fetch(`/api/users/${userId}`)
    .then(r => r.json())
    .safe();

  // Early return on error
  if (!result.success) {
    console.error('Failed to fetch user:', result.error);
    return null;
  }

  // Continue with success path
  const user = result.data;
  return enrichUserData(user);
}

Multiple Safe Operations

typescript
async function loadDashboardData() {
  // Execute multiple operations safely
  const [usersResult, postsResult, statsResult] = await Promise.all([
    Futurable.fetch('/api/users').then(r => r.json()).safe(),
    Futurable.fetch('/api/posts').then(r => r.json()).safe(),
    Futurable.fetch('/api/stats').then(r => r.json()).safe()
  ]);

  // Aggregate results
  return {
    users: usersResult.success ? usersResult.data : [],
    posts: postsResult.success ? postsResult.data : [],
    stats: statsResult.success ? statsResult.data : null,
    errors: [
      !usersResult.success && usersResult.error,
      !postsResult.success && postsResult.error,
      !statsResult.success && statsResult.error
    ].filter(Boolean)
  };
}

With React Hooks

typescript
import { useEffect, useState } from 'react';
import { Futurable } from '@ndriadev/futurable';

function UserProfile({ userId }) {
  const [state, setState] = useState({
    data: null,
    loading: true,
    error: null
  });

  useEffect(() => {
    const request = Futurable
      .fetch(`/api/users/${userId}`)
      .then(r => r.json())
      .safe()
      .then(result => {
        if (result.success) {
          setState({ data: result.data, loading: false, error: null });
        } else {
          setState({ data: null, loading: false, error: result.error });
        }
      });

    return () => request.cancel();
  }, [userId]);

  if (state.loading) return <div>Loading...</div>;
  if (state.error) return <div>Error: {state.error.message}</div>;
  return <div>{state.data.name}</div>;
}

Form Submission

typescript
async function handleSubmit(formData: FormData) {
  const result = await Futurable.fetch('/api/submit', {
    method: 'POST',
    body: formData
  })
    .then(r => r.json())
    .safe();

  if (result.success) {
    showSuccessMessage('Form submitted successfully!');
    return result.data;
  } else {
    showErrorMessage(`Submission failed: ${result.error}`);
    return null;
  }
}

Validation Chain

typescript
async function processUserInput(input: string) {
  // Validate and process in a chain
  const result = await Futurable.resolve(input)
    .then(val => {
      if (!val) throw new Error('Input is required');
      return val.trim();
    })
    .then(val => {
      if (val.length < 3) throw new Error('Input too short');
      return val;
    })
    .then(val => validateAgainstAPI(val))
    .safe();

  if (result.success) {
    return { valid: true, value: result.data };
  } else {
    return { valid: false, error: result.error.message };
  }
}

Comparison with try-catch

Traditional try-catch

typescript
async function fetchData() {
  try {
    const response = await fetch('/api/data');
    const data = await response.json();
    return { success: true, data };
  } catch (error) {
    return { success: false, error };
  }
}

With safe()

typescript
async function fetchData() {
  const result = await Futurable.fetch('/api/data')
    .then(r => r.json())
    .safe();

  return result; // Same shape, less code
}

Pattern: Result Monad

The safe() method implements the Result/Either monad pattern, common in functional programming:

typescript
// Helper functions for working with SafeResult
function map<T, U>(
  result: SafeResult<T>,
  fn: (value: T) => U
): SafeResult<U> {
  return result.success
    ? { success: true, data: fn(result.data), error: null }
    : result;
}

function flatMap<T, U>(
  result: SafeResult<T>,
  fn: (value: T) => SafeResult<U>
): SafeResult<U> {
  return result.success ? fn(result.data) : result;
}

// Usage
const result = await Futurable.fetch('/api/user')
  .then(r => r.json())
  .safe();

const transformed = map(result, user => user.name.toUpperCase());

Best Practices

1. Use for Expected Errors

typescript
// ✅ Good - network calls often fail
const result = await Futurable.fetch('/api/data')
  .then(r => r.json())
  .safe();

// ❌ Not needed - this shouldn't fail
const result = await Futurable.resolve(42).safe();

2. Always Check success

typescript
// ❌ Don't assume success
const result = await fetchData().safe();
console.log(result.data); // Could be null!

// ✅ Always check
const result = await fetchData().safe();
if (result.success) {
  console.log(result.data);
}

3. Type Error Appropriately

typescript
// ✅ Specific error type
interface ValidationError {
  field: string;
  message: string;
}

const result = await validateForm().safe<ValidationError>();

// ❌ Generic unknown
const result = await validateForm().safe(); // error is unknown

4. Combine with Early Returns

typescript
// ✅ Clean early return pattern
async function process(id: number) {
  const result = await fetchData(id).safe();
  if (!result.success) {
    logError(result.error);
    return null;
  }

  // Continue processing with result.data
  return transform(result.data);
}

Type Definitions

typescript
// SafeResult type
type SafeResult<T, E = Error> =
  | { success: true; data: T; error: null }
  | { success: false; data: null; error: E };

// Method signature
safe<TError = unknown>(): Futurable<SafeResult<T, TError>>

Notes

  • The Futurable never rejects when using safe()
  • Always resolves with a SafeResult object
  • TypeScript discriminated unions work perfectly with the success flag
  • Can be chained with other Futurable methods before calling safe()
  • The error type can be customized with the generic parameter
  • Cancellation still works as expected

See Also

Released under the MIT License.