Back in the lessons on Context.Tag (16)
and Effect.provideService (17)
we saw how to define a custom service to help with dependency injection. The Effect library includes several “default” services. The Console service wraps writing to the console.
Console.log is an Effectful version of console.log. This means that it represents an instruction, which will only run when passed to one of the Effect.run*. This is different from a regular console.log, which will execute immediately.
Executes an effect synchronously, running it immediately and returning the
result.
Details
This function evaluates the provided effect synchronously, returning its
result directly. It is ideal for effects that do not fail or include
asynchronous operations. If the effect does fail or involves async tasks, it
will throw an error. Execution stops at the point of failure or asynchronous
operation, making it unsuitable for effects that require asynchronous
handling.
Important: Attempting to run effects that involve asynchronous operations
or failures will result in exceptions being thrown, so use this function with
care for purely synchronous and error-free effects.
When to Use
Use this function when:
You are sure that the effect will not fail or involve asynchronous
operations.
You need a direct, synchronous result from the effect.
You are working within a context where asynchronous effects are not
allowed.
Avoid using this function for effects that can fail or require asynchronous
handling. For such cases, consider using
runPromise
or
runSyncExit
.
Example (Synchronous Logging)
import { Effect } from"effect"
constprogram= Effect.sync(() => {
console.log("Hello, World!")
return1
})
constresult= Effect.runSync(program)
// Output: Hello, World!
console.log(result)
// Output: 1
Example (Incorrect Usage with Failing or Async Effects)
import { Effect } from"effect"
try {
// Attempt to run an effect that fails
Effect.runSync(Effect.fail("my error"))
} catch (e) {
console.error(e)
}
// Output:
// (FiberFailure) Error: my error
try {
// Attempt to run an effect that involves async work
// (FiberFailure) AsyncFiberException: Fiber #0 cannot be resolved synchronously. This is caused by using runSync on an effect that performs async work
@see ― runSyncExit for a version that returns an Exit type instead of
throwing an error.
Executes an effect synchronously, running it immediately and returning the
result.
Details
This function evaluates the provided effect synchronously, returning its
result directly. It is ideal for effects that do not fail or include
asynchronous operations. If the effect does fail or involves async tasks, it
will throw an error. Execution stops at the point of failure or asynchronous
operation, making it unsuitable for effects that require asynchronous
handling.
Important: Attempting to run effects that involve asynchronous operations
or failures will result in exceptions being thrown, so use this function with
care for purely synchronous and error-free effects.
When to Use
Use this function when:
You are sure that the effect will not fail or involve asynchronous
operations.
You need a direct, synchronous result from the effect.
You are working within a context where asynchronous effects are not
allowed.
Avoid using this function for effects that can fail or require asynchronous
handling. For such cases, consider using
runPromise
or
runSyncExit
.
Example (Synchronous Logging)
import { Effect } from"effect"
constprogram= Effect.sync(() => {
console.log("Hello, World!")
return1
})
constresult= Effect.runSync(program)
// Output: Hello, World!
console.log(result)
// Output: 1
Example (Incorrect Usage with Failing or Async Effects)
import { Effect } from"effect"
try {
// Attempt to run an effect that fails
Effect.runSync(Effect.fail("my error"))
} catch (e) {
console.error(e)
}
// Output:
// (FiberFailure) Error: my error
try {
// Attempt to run an effect that involves async work
// (FiberFailure) AsyncFiberException: Fiber #0 cannot be resolved synchronously. This is caused by using runSync on an effect that performs async work
@see ― runSyncExit for a version that returns an Exit type instead of
throwing an error.
map takes a function and applies it to the value contained within an
effect, creating a new effect with the transformed value.
It's important to note that effects are immutable, meaning that the original
effect is not modified. Instead, a new effect is returned with the updated
value.
Runs a side effect with the result of an effect without changing the original
value.
Details
This function works similarly to flatMap, but it ignores the result of the
function passed to it. The value from the previous effect remains available
for the next part of the chain. Note that if the side effect fails, the
entire chain will fail too.
When to Use
Use this function when you want to perform a side effect, like logging or
tracking, without modifying the main value. This is useful when you need to
observe or record an action but want the original value to be passed to the
next step.
Example (Logging a step in a pipeline)
import { Console, Effect, pipe } from"effect"
// Function to apply a discount safely to a transaction amount
constapplyDiscount= (
total:number,
discountRate:number
):Effect.Effect<number, Error> =>
discountRate ===0
? Effect.fail(newError("Discount rate cannot be zero"))
map takes a function and applies it to the value contained within an
effect, creating a new effect with the transformed value.
It's important to note that effects are immutable, meaning that the original
effect is not modified. Instead, a new effect is returned with the updated
value.
Runs a side effect with the result of an effect without changing the original
value.
Details
This function works similarly to flatMap, but it ignores the result of the
function passed to it. The value from the previous effect remains available
for the next part of the chain. Note that if the side effect fails, the
entire chain will fail too.
When to Use
Use this function when you want to perform a side effect, like logging or
tracking, without modifying the main value. This is useful when you need to
observe or record an action but want the original value to be passed to the
next step.
Example (Logging a step in a pipeline)
import { Console, Effect, pipe } from"effect"
// Function to apply a discount safely to a transaction amount
constapplyDiscount= (
total:number,
discountRate:number
):Effect.Effect<number, Error> =>
discountRate ===0
? Effect.fail(newError("Discount rate cannot be zero"))
map takes a function and applies it to the value contained within an
effect, creating a new effect with the transformed value.
It's important to note that effects are immutable, meaning that the original
effect is not modified. Instead, a new effect is returned with the updated
value.
} |undefined) => <A, E, R>(self:Effect.Effect<...>) =>Effect.Effect<...> (+1overload)
Executes two effects sequentially, returning the result of the first effect
and ignoring the result of the second.
Details
This function allows you to run two effects in sequence, where the result of
the first effect is preserved, and the result of the second effect is
discarded. By default, the two effects are executed sequentially. If you need
them to run concurrently, you can pass the { concurrent: true } option.
The second effect will always be executed, even though its result is ignored.
This makes it useful for cases where you want to execute an effect for its
side effects while keeping the result of another effect.
When to Use
Use this function when you are only interested in the result of the first
effect but still need to run the second effect for its side effects, such as
logging or performing a cleanup action.
map takes a function and applies it to the value contained within an
effect, creating a new effect with the transformed value.
It's important to note that effects are immutable, meaning that the original
effect is not modified. Instead, a new effect is returned with the updated
value.
Executes an effect synchronously, running it immediately and returning the
result.
Details
This function evaluates the provided effect synchronously, returning its
result directly. It is ideal for effects that do not fail or include
asynchronous operations. If the effect does fail or involves async tasks, it
will throw an error. Execution stops at the point of failure or asynchronous
operation, making it unsuitable for effects that require asynchronous
handling.
Important: Attempting to run effects that involve asynchronous operations
or failures will result in exceptions being thrown, so use this function with
care for purely synchronous and error-free effects.
When to Use
Use this function when:
You are sure that the effect will not fail or involve asynchronous
operations.
You need a direct, synchronous result from the effect.
You are working within a context where asynchronous effects are not
allowed.
Avoid using this function for effects that can fail or require asynchronous
handling. For such cases, consider using
runPromise
or
runSyncExit
.
Example (Synchronous Logging)
import { Effect } from"effect"
constprogram= Effect.sync(() => {
console.log("Hello, World!")
return1
})
constresult= Effect.runSync(program)
// Output: Hello, World!
console.log(result)
// Output: 1
Example (Incorrect Usage with Failing or Async Effects)
import { Effect } from"effect"
try {
// Attempt to run an effect that fails
Effect.runSync(Effect.fail("my error"))
} catch (e) {
console.error(e)
}
// Output:
// (FiberFailure) Error: my error
try {
// Attempt to run an effect that involves async work
// (FiberFailure) AsyncFiberException: Fiber #0 cannot be resolved synchronously. This is caused by using runSync on an effect that performs async work
@see ― runSyncExit for a version that returns an Exit type instead of
throwing an error.
@since ― 2.0.0
runSync(
constprogram:Effect.Effect<number, never, never>
program);
This will print
Terminal window
1
Now x is 2
This will be printed when x is 3
Console has other benefits beyond lazy execution and pipe-friendly ergonomics. Just like any other service, we can swap out the implementation. For example, we may want to provide a special TestConsole implementation to check logs during tests. We could also provide a custom Console that changes the format of log messages, perhaps by automatically adding a timestamp, or converting all messages to a JSON format for structured logging.
Effect v3.x implements these “default services” using a FiberRef. This may change in Effect v4.0. We won’t go into too much detail on this, since we haven’t yet covered Fibers or Refs. For now, it is enough to know that somewhere behind the scenes, a concrete default implementation of the Console is being provided.
We can implement an approximate version of Console.log using the tools we have learned so far.