This guide provides detailed documentation beyond the basics covered in the README.
Taski provides comprehensive error handling for parallel task execution.
| Exception | Purpose |
|---|---|
Taski::AggregateError |
Multiple tasks failed during parallel execution |
Taski::TaskError |
Base class for task-specific errors |
Taski::TaskAbortException |
Intentional abort (stops all tasks immediately) |
Taski::CircularDependencyError |
Circular dependency detected |
TaskClass::Error |
Auto-generated error class for each Task subclass |
When multiple tasks fail during parallel execution, errors are collected into an AggregateError:
class DatabaseTask < Taski::Task
exports :connection
def run
raise "Database connection failed"
end
end
class CacheTask < Taski::Task
exports :redis_client
def run
raise "Cache connection failed"
end
end
class AppTask < Taski::Task
def run
db = DatabaseTask.connection
cache = CacheTask.redis_client
end
end
begin
AppTask.run
rescue Taski::AggregateError => e
puts "#{e.errors.size} tasks failed:"
e.errors.each do |failure|
puts " - #{failure.task_class.name}: #{failure.error.message}"
end
end
# Output:
# 2 tasks failed:
# - DatabaseTask: Database connection failed
# - CacheTask: Cache connection failedEach Task subclass automatically gets an ::Error class for targeted rescue:
class DatabaseTask < Taski::Task
exports :connection
def run
raise "Connection failed"
end
end
# Rescue errors from a specific task
begin
AppTask.run
rescue DatabaseTask::Error => e
puts "Database task failed: #{e.message}"
# e.task_class returns DatabaseTask
# e.cause returns the original error
endThis works transparently with AggregateError — when you rescue DatabaseTask::Error, it matches an AggregateError that contains a DatabaseTask::Error.
How this works: Ruby's rescue uses the === operator to match exceptions. Taski's AggregateAware module (extended by TaskError and all TaskClass::Error classes) overrides === to check whether an AggregateError contains an error of that type. This means rescue Taski::TaskError will match an AggregateError wrapping TaskError instances, even though AggregateError does not inherit from TaskError.
Use TaskAbortException to immediately stop all task execution:
class CriticalTask < Taski::Task
def run
if critical_condition_met?
raise Taski::TaskAbortException, "Critical error - aborting"
end
end
endTaskAbortException takes priority over regular errors. Already running tasks will complete, but no new tasks will start.
# 1. Handle errors within the task when recovery is possible
class ResilientTask < Taski::Task
exports :data
def run
@data = fetch_from_primary
rescue Timeout::Error
@data = fetch_from_fallback
end
end
# 2. Use task-specific errors for clarity
begin
AppTask.run
rescue DatabaseTask::Error => e
handle_database_failure(e)
rescue CacheTask::Error => e
handle_cache_failure(e)
end
# 3. Fail fast with clear messages
class ValidatingTask < Taski::Task
def run
missing = %w[DATABASE_URL API_KEY].select { |v| ENV[v].nil? }
raise "Missing: #{missing.join(', ')}" if missing.any?
end
endTaski supports resource cleanup with run_and_clean, which executes the run phase followed by the clean phase in a single operation.
class DatabaseSetup < Taski::Task
exports :connection
def run
@connection = "postgresql://localhost:5432/myapp"
puts "Database connected"
end
def clean
puts "Database disconnected"
end
end
class WebServer < Taski::Task
def run
puts "Server started with #{DatabaseSetup.connection}"
end
def clean
puts "Server stopped"
end
end
# Run then clean in one call
WebServer.run_and_clean
# => Database connected
# => Server started
# => Server stopped
# => Database disconnectedUse a block to execute code between run and clean phases. This is useful when you need to use exported values before cleanup:
DatabaseSetup.run_and_clean do
# Exported values are accessible here
deploy(DatabaseSetup.connection)
end
# Clean runs automatically after the blockClean methods should be safe to call multiple times:
class SafeFileTask < Taski::Task
exports :data_file
def run
@data_file = '/tmp/data.txt'
File.write(@data_file, 'data')
end
def clean
# Check before delete
if @data_file && File.exist?(@data_file)
File.delete(@data_file)
end
end
endTaski provides real-time progress visualization during task execution.
- Spinner Animation: Animated spinner during execution
- Output Capture: Real-time display of task output (last line)
- Status Indicators: Success/failure icons with execution time
- Group Blocks: Organize output messages into logical phases
- TTY Detection: Clean output when redirected to files
Use group blocks to organize output within a task into logical phases. The current group name is displayed alongside the task's output in the progress display.
class DeployTask < Taski::Task
def run
group("Preparing environment") do
puts "Checking dependencies..."
puts "Validating config..."
end
group("Building application") do
puts "Compiling source..."
puts "Running tests..."
end
group("Deploying") do
puts "Uploading files..."
puts "Restarting server..."
end
end
endProgress display output:
During execution:
⠋ DeployTask (Task) | Deploying: Uploading files...
After completion:
✓ DeployTask (Task) 520ms
The group name appears as a prefix to the output message: | GroupName: output...
Groups are useful for:
- Logical organization: Group related operations together
- Progress visibility: See which phase is currently executing
- Error context: Know which phase failed when errors occur
During execution:
WebServer (Task)
├── Config (Task) ...
│ ├── Database (Task) 45.2ms
│ └── Cache (Task) ...
└── Server (Task)
After completion:
WebServer (Task) 120.5ms
├── Config (Task) 50.3ms
│ ├── Database (Task) 45.2ms
│ └── Cache (Task) 48.1ms
└── Server (Task) 70.2ms
Taski supports three progress display modes:
Compact single-line display showing current progress:
⠹ [3/5] DeployTask | Uploading files...
✓ [5/5] All tasks completed (1234ms)
Format: [spinner] [completed/total] TaskName | last output...
When multiple tasks run in parallel:
⠹ [2/5] DownloadLayer1, DownloadLayer2 | Downloading...
On failure:
✗ [3/5] DeployTask failed: Connection refused
Full dependency tree visualization with status for each task:
WebServer (Task)
├── ⠋ Config (Task) | Reading config.yml...
│ ├── ✅ Database (Task) 45.2ms
│ └── ⠙ Cache (Task) | Connecting...
└── ◻ Server (Task)
Plain text output without escape codes, designed for CI/logs:
[START] DatabaseSetup
[DONE] DatabaseSetup (45.2ms)
[START] WebServer
[DONE] WebServer (120.5ms)
[TASKI] Completed: 2/2 tasks (165ms)
Taski.progress_display = Taski::Progress::Layout::Simple.new # Simple display (default)
Taski.progress_display = Taski::Progress::Layout::Tree.new # Tree display
Taski.progress_display = Taski::Progress::Layout::Log.new # Log output (CI/logs)
Taski.progress_display = nil # DisableWhen output is redirected, interactive spinners are automatically disabled:
ruby build.rb > build.log 2>&1When a task accesses a dependency's exported value (e.g., DepTask.value), Taski may return a lightweight proxy object instead of the actual value. This proxy defers dependency resolution until you call a method on it, at which point it transparently resolves the real value and forwards the method call.
class FetchData < Taski::Task
exports :data
def run
@data = expensive_api_call
end
end
class ProcessData < Taski::Task
exports :result
def run
raw = FetchData.data # May return a proxy (no blocking yet)
setup_environment # Task continues while FetchData runs
@result = raw.transform # Proxy resolves here — blocks if needed
end
endFrom the user's perspective, the proxy is completely transparent — it behaves exactly like the real value.
Proxy-based resolution enables better parallelism. A task can continue executing setup logic while its dependencies are still running, only blocking when the dependency value is actually used. This can significantly reduce total execution time when tasks have independent setup work before they need their dependencies.
Taski uses static analysis (Prism AST parsing) to determine when proxy resolution is safe. Dependencies used in positions where the proxy could cause issues — such as conditions (if dep_value), method arguments, or other contexts where truthiness or identity matters — are automatically resolved synchronously instead of returning a proxy.
You do not need to think about this in normal usage. The static analyzer examines your task's run method and only enables proxy resolution for dependency accesses that are confirmed safe (e.g., simple assignments like x = Dep.value followed by method calls on x).
require "logger"
Taski.logger = Logger.new($stdout, level: Logger::DEBUG)Set Taski.logger to a Ruby Logger instance to enable structured logging of execution events.
puts MyTask.tree
# MyTask (Task)
# ├── DatabaseTask (Task)
# └── CacheTask (Task)
# └── ConfigTask (Task)Circular Dependencies
# Detected before execution
begin
TaskA.run
rescue Taski::CircularDependencyError => e
puts e.cyclic_tasks # [[TaskA, TaskB]]
endStatic Analysis Requirements
Tasks must be defined in source files (not dynamically with Class.new) because static analysis uses Prism AST parsing which requires actual source files. Static analysis is used for dependency tree visualization, circular dependency detection, and optimizing dependency resolution (determining when lazy proxy resolution is safe vs. when synchronous resolution is required).