Skip to content

memoize() ​

Cache the result of the first execution and reuse it for subsequent runs.

Syntax ​

typescript
task.memoize(catchErrors?: boolean): FuturableTask<T>

Parameters ​

catchErrors (optional) ​

  • false (default): Only cache successful results
  • true: Cache both success and error results

Return Value ​

A new FuturableTask<T> that caches its result.

Description ​

The memoize() method caches the result of the first execution. Subsequent calls to run() will return the cached result without re-executing the task.

This is useful for expensive operations that should only execute once.

Examples ​

Basic Memoization ​

typescript
let counter = 0;

const task = FuturableTask
  .of(() => {
    console.log('Computing...');
    return ++counter;
  })
  .memoize();

await task.run(); // Logs: "Computing...", returns 1
await task.run(); // Returns 1 (cached, no log)
await task.run(); // Returns 1 (cached, no log)

Expensive API Call ​

typescript
const fetchConfig = FuturableTask
  .fetch('/api/config')
  .map(res => res.json())
  .memoize();

// First call - fetches from API
const config1 = await fetchConfig.run();

// Subsequent calls - use cached result
const config2 = await fetchConfig.run();
const config3 = await fetchConfig.run();

Caching Errors ​

typescript
let attempts = 0;

const unstableTask = FuturableTask
  .of(() => {
    attempts++;
    if (attempts === 1) {
      throw new Error('First attempt failed');
    }
    return 'success';
  })
  .memoize(false); // Don't cache errors

try {
  await unstableTask.run(); // Fails
} catch (error) {
  console.log('First attempt failed');
}

const result = await unstableTask.run(); // Tries again, succeeds

Caching Both Success and Errors ​

typescript
const task = FuturableTask
  .of(() => {
    throw new Error('Always fails');
  })
  .memoize(true); // Cache errors too

try {
  await task.run(); // Fails and caches the error
} catch (error) {
  console.log('Failed:', error);
}

try {
  await task.run(); // Returns cached error
} catch (error) {
  console.log('Cached error:', error);
}

Use Cases ​

Configuration Loading ​

typescript
const loadAppConfig = FuturableTask
  .of(async () => {
    console.log('Loading configuration...');
    const res = await fetch('/api/config');
    return res.json();
  })
  .memoize();

// Use throughout your app
const config = await loadAppConfig.run();

Reference Data ​

typescript
const getCountries = FuturableTask
  .of(() => fetch('/api/countries'))
  .map(res => res.json())
  .memoize();

const getLanguages = FuturableTask
  .of(() => fetch('/api/languages'))
  .map(res => res.json())
  .memoize();

// These only fetch once
const countries = await getCountries.run();
const languages = await getLanguages.run();

Expensive Computation ​

typescript
const computeStatistics = FuturableTask
  .of(() => {
    console.log('Computing statistics...');
    // Complex, time-consuming calculations
    return analyzeData(largeDataset);
  })
  .memoize();

// First call computes
const stats1 = await computeStatistics.run();

// Subsequent calls are instant
const stats2 = await computeStatistics.run();

Singleton Pattern ​

typescript
class DatabaseConnection {
  static connect = FuturableTask
    .of(async () => {
      console.log('Connecting to database...');
      return await createConnection();
    })
    .memoize();
}

// Only connects once
const conn1 = await DatabaseConnection.connect.run();
const conn2 = await DatabaseConnection.connect.run(); // Same instance

Behavior Details ​

Caching Success Only (default) ​

typescript
let attempts = 0;

const task = FuturableTask
  .of(() => {
    attempts++;
    if (attempts < 3) {
      throw new Error('Failed');
    }
    return 'success';
  })
  .memoize(); // catchErrors = false

try {
  await task.run(); // Fails, not cached
} catch {}

try {
  await task.run(); // Fails, not cached
} catch {}

const result = await task.run(); // Succeeds, cached
const result2 = await task.run(); // Returns cached success

Caching Everything ​

typescript
const task = FuturableTask
  .of(() => {
    throw new Error('Error');
  })
  .memoize(true); // Cache errors

try {
  await task.run(); // Fails, error is cached
} catch {}

try {
  await task.run(); // Returns cached error immediately
} catch {}

Comparison with Non-Memoized ​

typescript
// Without memoization
const nonMemoized = FuturableTask.of(() => {
  console.log('Executing');
  return expensiveOperation();
});

await nonMemoized.run(); // Logs: "Executing"
await nonMemoized.run(); // Logs: "Executing" again
await nonMemoized.run(); // Logs: "Executing" again

// With memoization
const memoized = FuturableTask.of(() => {
  console.log('Executing');
  return expensiveOperation();
}).memoize();

await memoized.run(); // Logs: "Executing"
await memoized.run(); // No log (cached)
await memoized.run(); // No log (cached)

Performance Benefits ​

typescript
// Slow operation without memoization
const slow = FuturableTask.of(() => {
  let sum = 0;
  for (let i = 0; i < 1000000000; i++) {
    sum += i;
  }
  return sum;
});

console.time('Run 1');
await slow.run();
console.timeEnd('Run 1'); // ~500ms

console.time('Run 2');
await slow.run();
console.timeEnd('Run 2'); // ~500ms again

// Fast with memoization
const fast = slow.memoize();

console.time('First');
await fast.run();
console.timeEnd('First'); // ~500ms

console.time('Second');
await fast.run();
console.timeEnd('Second'); // <1ms (cached)

Best Practices ​

1. Memoize Pure Operations ​

typescript
// ✅ Good - deterministic result
const getPiValue = FuturableTask
  .of(() => Math.PI)
  .memoize();

// ❌ Bad - non-deterministic
const getCurrentTime = FuturableTask
  .of(() => Date.now())
  .memoize(); // Will always return first timestamp

2. Memoize Expensive Operations ​

typescript
// ✅ Good - expensive to compute
const parseHugeFile = FuturableTask
  .of(() => parseJSON(hugeFile))
  .memoize();

// ❌ Bad - cheap operation
const addNumbers = FuturableTask
  .of(() => 1 + 2)
  .memoize(); // Overhead not worth it

3. Consider Memory Usage ​

typescript
// ✅ Good - small result
const getConfig = FuturableTask
  .of(() => loadConfig())
  .memoize();

// ⚠️ Careful - large result stays in memory
const loadEntireDatabase = FuturableTask
  .of(() => fetchAllRecords())
  .memoize(); // May cause memory issues

4. Cache Errors Selectively ​

typescript
// ✅ Good - don't cache transient errors
const fetchData = FuturableTask
  .fetch('/api/data')
  .memoize(false); // Retry on network errors

// ✅ Good - cache validation errors
const validateInput = FuturableTask
  .of(() => validate(input))
  .memoize(true); // Input won't change

Notes ​

  • Caching happens on first successful execution (or first execution if catchErrors: true)
  • Cached value persists for the lifetime of the task instance
  • Each task instance has its own cache
  • No way to invalidate/clear the cache
  • Memory is held until task is garbage collected
  • Works with all other FuturableTask methods

See Also ​

Released under the MIT License.