Skip to main content

Getting Started

go-go-scope provides structured concurrency using TypeScript’s Explicit Resource Management (the using and await using keywords). All concurrent operations are automatically cleaned up when their scope completes.
Requires Node.js 24+ or Bun 1.2+ with TypeScript 5.2+ for using/await using support.

Creating a Scope

Every concurrent operation starts with a scope. Scopes automatically cancel all tasks and clean up resources when disposed.
import { scope } from 'go-go-scope';

// Basic scope with automatic cleanup
await using s = scope();

// Scope with timeout
await using s = scope({ timeout: 5000 });

// Scope with name for debugging
await using s = scope({ name: 'api-request' });
Always use await using (not using) for scopes, since cleanup is asynchronous.

Running Tasks

Tasks are the fundamental unit of concurrent work. They return a Result tuple [error, value] instead of throwing exceptions.

Basic Task Execution

await using s = scope();

const task = s.task(async ({ signal }) => {
  const response = await fetch('https://api.example.com/data', { signal });
  return response.json();
});

const [err, data] = await task;

if (err) {
  console.error('Request failed:', err);
} else {
  console.log('Data:', data);
}

Accessing Scope Services

Tasks receive a context object with signal, logger, and services:
await using s = scope({ name: 'api-service' });

const [err, result] = await s.task(async ({ signal, logger, context }) => {
  logger.info('Starting request');
  
  // Access context values
  const userId = context.userId;
  
  const response = await fetch(`/api/users/${userId}`, { signal });
  return response.json();
});

Timeouts

Timeouts can be applied at the scope level or per-task.
// All tasks inherit the scope timeout
await using s = scope({ timeout: 5000 });

const [err, data] = await s.task(async ({ signal }) => {
  return fetch('https://slow-api.com', { signal });
});

if (err) {
  // Error: "Scope timeout after 5000ms"
}

Retry Logic

Retry failed operations with configurable delay strategies.
1

Simple Retry with Exponential Backoff

import { scope, exponentialBackoff } from 'go-go-scope';

await using s = scope();

const [err, data] = await s.task(
  async ({ signal }) => {
    const res = await fetch('https://api.example.com/data', { signal });
    if (!res.ok) throw new Error('API error');
    return res.json();
  },
  {
    retry: {
      maxRetries: 5,
      delay: exponentialBackoff({ initial: 100, max: 5000, jitter: 0.3 }),
      onRetry: (error, attempt) => {
        console.log(`Retry attempt ${attempt}: ${error.message}`);
      }
    }
  }
);
2

Conditional Retry

const [err, data] = await s.task(
  async () => apiCall(),
  {
    retry: {
      maxRetries: 3,
      delay: 1000,
      // Only retry on specific errors
      retryCondition: (error) => {
        return error instanceof NetworkError || error.status === 503;
      }
    }
  }
);
3

Shorthand Retry Strategies

// Exponential backoff (default)
const [err1, data1] = await s.task(() => apiCall(), { retry: 'exponential' });

// Linear backoff
const [err2, data2] = await s.task(() => apiCall(), { retry: 'linear' });

// Fixed delay
const [err3, data3] = await s.task(() => apiCall(), { retry: 'fixed' });

Error Handling

Result Tuples

Tasks return [error, value] tuples instead of throwing:
const [err, user] = await s.task(async () => fetchUser(id));

if (err) {
  // Handle error
  console.error(err);
  return;
}

// Use value (TypeScript knows err is undefined here)
console.log(user.name);

Custom Error Classes

Wrap errors in custom classes for better type safety:
class APIError extends Error {
  constructor(message: string, public code: number) {
    super(message);
    this.name = 'APIError';
  }
}

const [err, data] = await s.task(
  async () => {
    const res = await fetch('/api/data');
    if (!res.ok) {
      throw new APIError('Request failed', res.status);
    }
    return res.json();
  },
  { errorClass: APIError }
);

if (err) {
  // err is typed as APIError
  console.log(`API error: ${err.code}`);
}

Cancellation

All tasks receive an AbortSignal for cooperative cancellation:
await using s = scope({ timeout: 5000 });

const task1 = s.task(async ({ signal }) => {
  // Pass signal to fetch
  const res = await fetch('https://api.example.com', { signal });
  return res.json();
});

const task2 = s.task(async ({ signal }) => {
  // Manual cancellation check
  for (let i = 0; i < 1000; i++) {
    if (signal.aborted) {
      throw new Error('Cancelled');
    }
    await processItem(i);
  }
});

// Both tasks are automatically cancelled after 5 seconds
Always pass the signal to async operations like fetch() and check signal.aborted in loops.

Dependency Injection

Share services across tasks using dependency injection:
interface Services {
  db: Database;
  cache: Cache;
}

await using s = scope()
  .provide('db', new Database())
  .provide('cache', new Cache());

const [err, user] = await s.task(async ({ services }) => {
  // Access injected services
  const cached = await services.cache.get('user:1');
  if (cached) return cached;
  
  const user = await services.db.query('SELECT * FROM users WHERE id = 1');
  await services.cache.set('user:1', user);
  return user;
});

Logging

Built-in structured logging with automatic task context:
await using s = scope({ name: 'api-handler', logLevel: 'info' });

const [err, result] = await s.task(async ({ logger }) => {
  logger.info('Processing request');
  logger.debug('Detailed debug info');
  logger.warn('Something unusual');
  logger.error('An error occurred');
  
  return { status: 'ok' };
});

Common Patterns

Health Check with Timeout

async function checkHealth(): Promise<boolean> {
  await using s = scope({ timeout: 3000 });
  
  const [err] = await s.task(async ({ signal }) => {
    await Promise.all([
      fetch('https://api.example.com/health', { signal }),
      fetch('https://db.example.com/health', { signal }),
    ]);
  });
  
  return err === undefined;
}

Retry with Backoff

import { exponentialBackoff } from 'go-go-scope';

await using s = scope();

const [err, data] = await s.task(
  async () => unreliableAPI(),
  {
    retry: {
      maxRetries: 5,
      delay: exponentialBackoff({ initial: 100, max: 30000 })
    }
  }
);

Request with Context

await using s = scope({ 
  context: { 
    userId: '123', 
    requestId: 'req-456' 
  } 
});

const [err, result] = await s.task(async ({ context, logger }) => {
  logger.info('Request started', { userId: context.userId });
  return processRequest(context);
});

Next Steps

Channels

Learn about Go-style channels for producer-consumer patterns

Parallel Execution

Run multiple tasks concurrently with progress tracking

Resilience Patterns

Circuit breakers, retries, and fault tolerance

Streams

Lazy stream processing with 50+ operations

Build docs developers (and LLMs) love