---
title: "Best Practices"
description: "Learn the best practices for using Clack effectively"
canonical: https://bomb.sh/docs/clack/guides/best-practices/
---

# Best Practices

This guide covers recommended practices and patterns for using Clack effectively in your applications.

## General Guidelines

### 1. Error Handling

Always handle potential errors and cancellations:

```ts
import { text, isCancel } from '@clack/prompts';

try {
  const name = await text({
    message: 'What is your name?',
  });
  
  if (isCancel(name)) {
    console.log('Operation cancelled');
    process.exit(0);
  }
  
  // Use the name value
} catch (error) {
  console.error('An error occurred:', error);
  process.exit(1);
}
```

### 2. Input Validation

Implement proper validation for user inputs:

```ts
import { text } from '@clack/prompts';

// Define validation function outside for reusability
function validateAge(value: string | undefined): string | undefined {
  if (!value) return 'Please enter a value';
  const num = parseInt(value);
  if (isNaN(num)) return 'Please enter a valid number';
  if (num < 0 || num > 120) return 'Age must be between 0 and 120';
  return undefined;
}

const age = await text({
  message: 'Enter your age:',
  validate: validateAge,
});
```

Validators can be asynchronous (v1.8.0). Return a `Promise` to check against a remote source; the prompt blocks input and shows a validating state until it settles:

```ts
import { text } from '@clack/prompts';

declare function fetchTakenNames(): Promise<string[]>;

async function validateProjectName(value: string | undefined): Promise<string | undefined> {
  if (!value) return 'Please enter a name';
  const taken = await fetchTakenNames();
  if (taken.includes(value)) return `"${value}" is already taken`;
  return undefined;
}

const projectName = await text({
  message: 'Project name:',
  validate: validateProjectName,
});
```

A [Standard Schema](https://github.com/standard-schema/standard-schema) can be used, for example using [Arktype](https://arktype.io/):

```ts
import { text } from '@clack/prompts';
import { type } from 'arktype';

const name = await text({
  message: 'Enter your name (letters only)',
  validate: type('string.alpha').describe('Name can only contain letters'),
});
```

Async schemas work the same way (v1.8.0): a schema whose validation returns a `Promise`, such as an Arktype or Zod async refinement, is awaited before the prompt resolves.

### 3. Consistent Messaging

Use clear and consistent messaging across your prompts:

```ts
import { select, isCancel } from '@clack/prompts';

// Good
const userAction = await select({
  message: 'What would you like to do?',
  options: [
    { value: 'create', label: 'Create new project' },
    { value: 'update', label: 'Update existing project' },
  ],
});

if (isCancel(userAction)) {
  console.log('Operation cancelled');
  process.exit(0);
}

// Avoid
const quickAction = await select({
  message: 'Action:', // Too vague
  options: [
    { value: 'c', label: 'Create' }, // Inconsistent formatting
    { value: 'u', label: 'Update' },
  ],
});
```

### 4. Progressive Disclosure

Break complex interactions into smaller, manageable steps:

```ts
import { select, isCancel } from '@clack/prompts';

// First, get the project type
const projectType = await select({
  message: 'Select project type:',
  options: [
    { value: 'web', label: 'Web Application' },
    { value: 'cli', label: 'CLI Tool' },
  ],
});

if (isCancel(projectType)) {
  console.log('Operation cancelled');
  process.exit(0);
}

// Then, get specific details based on the type
if (projectType === 'web') {
  const framework = await select({
    message: 'Choose a framework:',
    options: [
      { value: 'react', label: 'React' },
      { value: 'vue', label: 'Vue' },
    ],
  });
}
```

### 5. Default Values

Provide sensible defaults when appropriate:

```ts
import { text, select, isCancel } from '@clack/prompts';

// Define types for better type safety
interface ProjectConfig {
  name: string;
  type: 'web' | 'cli';
  framework?: 'react' | 'vue';
  port?: number;
}

// Define validation functions
function validateProjectName(value: string | undefined): string | undefined {
  if (!value || value.length === 0) return 'Name is required';
  if (!/^[a-z0-9-]+$/.test(value)) return 'Name can only contain lowercase letters, numbers, and hyphens';
  return undefined;
}

// Collect project configuration with type safety
async function collectProjectConfig(): Promise<ProjectConfig> {
  const name = await text({
    message: 'Project name:',
    validate: validateProjectName,
  });

  if (isCancel(name)) {
    console.log('Operation cancelled');
    process.exit(0);
  }

  const type = await select({
    message: 'Project type:',
    options: [
      { value: 'web', label: 'Web Application' },
      { value: 'cli', label: 'CLI Tool' },
    ],
  });

  if (isCancel(type)) {
    console.log('Operation cancelled');
    process.exit(0);
  }

  const config: ProjectConfig = {
    name,
    type,
  };

  if (type === 'web') {
    const framework = await select({
      message: 'Choose a framework:',
      options: [
        { value: 'react', label: 'React' },
        { value: 'vue', label: 'Vue' },
      ],
    });

    if (isCancel(framework)) {
      console.log('Operation cancelled');
      process.exit(0);
    }

    config.framework = framework;
  }

  return config;
}
```

## Performance Considerations

### 1. Batch Operations

When dealing with multiple prompts, consider using `Promise.all` for parallel execution:

```ts
import { text } from '@clack/prompts';

const [userName, userEmail, userAge] = await Promise.all([
  text({ message: 'Name:' }),
  text({ message: 'Email:' }),
  text({ message: 'Age:' }),
]);
```

### 2. Lazy Loading

Load prompts only when needed:

```ts
import { select, isCancel } from '@clack/prompts';

// Mock function for demonstration
async function loadWebPrompts() {
  // This simulates loading web-specific prompts
  return {
    setupWebProject: async () => {
      // Implementation would go here
      return true;
    }
  };
}

async function setupProject() {
  const projectType = await select({
    message: 'Project type:',
    options: [
      { value: 'web', label: 'Web Application' },
      { value: 'cli', label: 'CLI Tool' },
    ],
  });

  if (isCancel(projectType)) {
    console.log('Operation cancelled');
    process.exit(0);
  }

  // Only load web-specific prompts if needed
  if (projectType === 'web') {
    const { setupWebProject } = await loadWebPrompts();
    await setupWebProject();
  }
}
```

## Accessibility

### 1. Clear Labels

Use descriptive labels for options:

```ts
import { select, isCancel } from '@clack/prompts';

// Good
const operationMode = await select({
  message: 'Select operation mode:',
  options: [
    { value: 'dev', label: 'Development Mode' },
    { value: 'prod', label: 'Production Mode' },
  ],
});

if (isCancel(operationMode)) {
  console.log('Operation cancelled');
  process.exit(0);
}

// Avoid
const mode = await select({
  message: 'Mode:',
  options: [
    { value: 'd', label: 'Dev' },
    { value: 'p', label: 'Prod' },
  ],
});
```

### 2. Helpful Error Messages

Provide clear error messages that guide users to correct input:

```ts
import { text } from '@clack/prompts';

const serverPort = await text({
  message: 'Enter port number:',
  validate: (value) => {
    if (!value) return 'Please enter a value';
    const num = parseInt(value);
    if (isNaN(num)) return 'Please enter a valid number';
    if (num < 1 || num > 65535) return 'Port must be between 1 and 65535';
    return undefined;
  },
});
```

## Advanced Patterns

### 1. Conditional Prompts

Use conditional logic to show relevant prompts:

```ts
import { text, select, isCancel } from '@clack/prompts';

interface DatabaseConfig {
  type: 'postgres' | 'mysql' | 'sqlite';
  host?: string;
  port?: number;
}

async function setupDatabase() {
  const dbType = await select({
    message: 'Select database type:',
    options: [
      { value: 'postgres', label: 'PostgreSQL' },
      { value: 'mysql', label: 'MySQL' },
      { value: 'sqlite', label: 'SQLite' },
    ],
  });

  if (isCancel(dbType)) {
    console.log('Operation cancelled');
    process.exit(0);
  }

  const config: DatabaseConfig = {
    type: dbType,
  };

  // Only ask for host and port if not SQLite
  if (dbType !== 'sqlite') {
    const host = await text({
      message: 'Database host:',
      defaultValue: 'localhost',
    });

    if (isCancel(host)) {
      console.log('Operation cancelled');
      process.exit(0);
    }

    const port = await text({
      message: 'Database port:',
      defaultValue: dbType === 'postgres' ? '5432' : '3306',
      validate: (value) => {
        if (!value) return 'Please enter a value';
        const num = parseInt(value);
        if (isNaN(num)) return 'Please enter a valid number';
        if (num < 1 || num > 65535) return 'Port must be between 1 and 65535';
        return undefined;
      },
    });

    if (isCancel(port)) {
      console.log('Operation cancelled');
      process.exit(0);
    }

    config.host = host;
    config.port = parseInt(port);
  }

  return config;
}
```

### 2. Reusable Prompt Functions

Create reusable functions for common prompt patterns:

```ts
import { text, select, isCancel } from '@clack/prompts';

async function promptForPort(defaultPort: string) {
  const port = await text({
    message: 'Enter port number:',
    defaultValue: defaultPort,
    validate: (value) => {
      if (!value) return 'Please enter a value';
      const num = parseInt(value);
      if (isNaN(num)) return 'Please enter a valid number';
      if (num < 1 || num > 65535) return 'Port must be between 1 and 65535';
      return undefined;
    },
  });

  if (isCancel(port)) {
    console.log('Operation cancelled');
    process.exit(0);
  }

  return parseInt(port);
}

// Use the reusable function
const serverPort = await promptForPort('3000');
const dbPort = await promptForPort('5432');
```
