| name | effect-patterns-scheduling |
| description | Effect-TS patterns for Scheduling. Use when working with scheduling in Effect-TS applications. |
Effect-TS Patterns: Scheduling
This skill provides 3 curated Effect-TS patterns for scheduling.
Use this skill when working on tasks related to:
- scheduling
- Best practices in Effect-TS applications
- Real-world patterns and solutions
🟢 Beginner Patterns
Retry Failed Operations
Rule: Use Effect.retry with a Schedule to handle transient failures gracefully.
Good Example:
import { Effect, Schedule, Data } from "effect"
class NetworkError extends Data.TaggedError("NetworkError")<{
readonly message: string
}> {}
class RateLimitError extends Data.TaggedError("RateLimitError")<{
readonly retryAfter: number
}> {}
class NotFoundError extends Data.TaggedError("NotFoundError")<{
readonly resource: string
}> {}
let callCount = 0
const fetchData = Effect.gen(function* () {
callCount++
yield* Effect.log(`API call attempt ${callCount}`)
if (callCount < 3) {
return yield* Effect.fail(new NetworkError({ message: "Connection timeout" }))
}
return { data: "Success!", attempts: callCount }
})
const withBasicRetry = fetchData.pipe(
Effect.retry(Schedule.recurs(5))
)
const withDelayedRetry = fetchData.pipe(
Effect.retry(
Schedule.spaced("500 millis").pipe(
Schedule.intersect(Schedule.recurs(5))
)
)
)
const fetchWithErrors = (shouldFail: boolean) =>
Effect.gen(function* () {
if (shouldFail) {
const random = Math.random()
if (random < 0.5) {
return yield* Effect.fail(new NetworkError({ message: "Timeout" }))
} else if (random < 0.8) {
return yield* Effect.fail(new RateLimitError({ retryAfter: 1000 }))
} else {
return yield* Effect.fail(new NotFoundError({ resource: "user:123" }))
}
}
return "Data fetched!"
})
const retryTransientOnly = fetchWithErrors(true).pipe(
Effect.retry({
schedule: Schedule.recurs(3),
while: (error) =>
error._tag === "NetworkError" || error._tag === "RateLimitError",
})
)
const withExponentialBackoff = fetchData.pipe(
Effect.retry(
Schedule.exponential("100 millis", 2).pipe(
Schedule.intersect(Schedule.recurs(5))
)
)
)
const program = Effect.gen(function* () {
yield* Effect.log("Starting retry demo...")
callCount = 0
const result = yield* withBasicRetry
yield* Effect.log(`Final result: ${JSON.stringify(result)}`)
})
Effect.runPromise(program)
Rationale:
Use Effect.retry to automatically retry operations that fail due to transient errors like network timeouts.
Many failures are temporary:
- Network issues - Connection drops, timeouts
- Rate limits - Too many requests
- Resource contention - Database locks
- Service restarts - Brief unavailability
Automatic retries handle these without manual intervention.
Your First Schedule
Rule: Use Schedule to control when and how often effects run.
Good Example:
import { Effect, Schedule } from "effect"
let attempts = 0
const flakyOperation = Effect.gen(function* () {
attempts++
if (attempts < 3) {
yield* Effect.log(`Attempt ${attempts} failed`)
return yield* Effect.fail(new Error("Temporary failure"))
}
return `Success on attempt ${attempts}`
})
const withRetry = flakyOperation.pipe(
Effect.retry(Schedule.recurs(5))
)
const logTime = Effect.gen(function* () {
const now = new Date().()
* .()
now
})
repeated = logTime.(
.(.())
)
polling = logTime.(
.(
.().(
.(.())
)
)
)
fixedDelay = .()
exponentialBackoff = .()
limitedAttempts = .()
retryPolicy = .().(
.(.())
)
program = .(* () {
* .()
result = * withRetry
* .()
* .()
* repeated
})
.(program)
Rationale:
Use Schedule to control timing in Effect programs - retrying failed operations, repeating successful ones, or adding delays.
Schedules solve common timing problems:
- Retries - Try again after failures
- Polling - Check for updates periodically
- Rate limiting - Control how fast things run
- Backoff - Increase delays between attempts
🟡 Intermediate Patterns
Scheduling Pattern 1: Repeat an Effect on a Fixed Interval
Rule: Repeat effects at fixed intervals using Schedule.fixed for steady-state operations and background tasks.
Good Example:
This example demonstrates a health check service that polls multiple service endpoints every 30 seconds and reports their status.
import { Effect, Schedule, Duration } from "effect";
interface ServiceStatus {
readonly service: string;
readonly url: string;
readonly isHealthy: boolean;
readonly responseTime: number;
readonly lastChecked: number;
}
const checkServiceHealth = (
url: string,
service: string
): Effect.Effect<ServiceStatus> =>
Effect.gen(function* () {
const startTime = Date.now();
const isHealthy = Math.random() > 0.1;
const responseTime = Math.random() * 500;
yield* Effect.(.(.(responseTime)));
(!isHealthy) {
* .( ());
}
{
service,
url,
: ,
: .(.() - startTime),
: .(),
};
});
{
: <{
: ;
: ;
}>;
: ;
}
serviceStatuses = <, >();
checkAllServices = (
:
): .<> =>
.(* () {
( service config.) {
status = * (service., service.).(
.
);
(status. === ) {
serviceStatuses.(service., status.);
.(
);
} {
.();
}
}
});
createHealthCheckScheduler = (
:
): .<> =>
(config).(
.(
.(.(config.))
)
);
reportStatus = (): .<> =>
.( {
(serviceStatuses. === ) {
.();
;
}
.();
( [service, status] serviceStatuses) {
ago = .((.() - status.) / );
.(
);
}
});
program = .(* () {
: = {
: [
{ : , : },
{ : , : },
{ : , : },
],
: ,
};
checker = * (config).(
.
);
* ().(
.(
.(
.(),
.()
)
)
);
* checker.();
});
.(program);
This pattern:
- Defines service health checks that may fail
- Uses Schedule.fixed to repeat every 5 seconds
- Handles failures gracefully (keeps last known status)
- Runs in background while main logic continues
- Reports current status at intervals
Rationale:
When you need to run an effect repeatedly at regular intervals (e.g., every 5 seconds, every 30 minutes), use Schedule.fixed to specify the interval. This creates a schedule that repeats the effect indefinitely or until a condition stops it, with precise timing between executions.
Many production systems need periodic operations:
- Health checks: Poll service availability every 30 seconds
- Cache refresh: Update cache every 5 minutes
- Metrics collection: Gather system metrics every 10 seconds
- Data sync: Sync data with remote service periodically
- Cleanup tasks: Remove stale data nightly
Without proper scheduling:
- Manual polling with
while loops wastes CPU (busy-waiting)
- Thread.sleep blocks threads, preventing other work
- No automatic restart on failure
- Difficult to test deterministically
With Schedule.fixed:
- Efficient, non-blocking repetition
- Automatic failure handling and retry
- Testable with TestClock
- Clean, declarative syntax