Cats Effect 3 for Beginners [Part-1]

Cats Effect 3 for Beginners [Part-1]

For Scala developers who are new to Cats Effect, this article series aims to provide a comprehensive guide on how to use the effect system that helps control the execution of events. The series covers the concept of an effect, the importance of an effect system, and a step-by-step guide on creating and executing effects using the Cats Effect. Additionally, it delves into the IO Monad and its diverse range of operations that can be performed.


Cats Effect is one of the most popular effect systems in Scala. In this series of articles, let's look at some of the most important features of Cats Effect.

This series is intended for Scala developers who are complete beginners to Cats Effect. However, I assume that the readers know the basics of Scala and have mostly worked with scala's Future. Note that, this is not intended to explain all the features of Cats Effect, but merely to get started with Cats Effect. There are numerous other blogs, youtube videos and code samples that explains the features in deep.

What is an Effect ?

Effect is a description of an action, rather than the action itself. It describes what may happen when the action is executed.

What is the need for an Effect System?

Effect system helps the developers to control the execution. We can describe the events in detail and compose multiple effects to create a long chain of related effects. We can then execute the effects only when it is needed. That means the effect systems allows us to handle the events lazily. That is not possible using scala's Future.

Let's look at it with the common example:

val futurePrint: Future[Unit] = Future(println("Hello from the Future"))

This code will immediately start execution, eagerly. We cant control when the future should be executed. This is where effect systems like Cats-Effect comes in picture. It helps to describe the computation, but execute them on demand.

IO is the most important and common effect in Cats-Effect. We will be using the latest Cats Effect v3 for this series. For that we can add the dependency:

 libraryDependencies += "org.typelevel" %% "cats-effect" % "3.3.11"

How to Create IO?

There are multiple ways to create an IO data structure. Let's look some of the ways.

Using pure Method

Similar to Future.successful, IO also support pure method to lift an already executed value into IO type.

val scalaIO: IO[String] = IO.pure("Scala")

Note that, IO.pure() method should be used only if the value is already computed and available.

Using apply Method

Like all other types in Scala, we can also use the apply method of IO to create IO effect:

val scalaIOApply: IO[String] = IO("Scala")

Using delay Method

We can use the IO.delay() method also to create IO data structure. It is exactly same as invoking the apply method.

val scalaIODelayed: IO[String] = IO.delay("Scala")

There are more advanced ways to create IO types, but we will look at them in later sections.

IO Monad

IO is a Monad. We don't really need to go in deep on the theoretical concepts of Monads. For now, we can assume that IO is a container type like Option, Either, Future etc. We can apply the common methods like map, flatMap, identity etc on IO types as well. Since it has flatMap and identity, we can use for-comprehension on IO types just like Future.

Executing the Effect

Now, let's look at how we can execute the effect we created before. We have mainly 3 ways to get started.

Using Scala App and main() method

We can write a normal Scala object and extend with App or implement the main method. Then we can use the method unsafeRunSync() on IO to execute the effect. However, we need to provide an implicit IORunTime instance. We can provide it by importing

import cats.effect.IO
object CatsEffectScalaApp {
  val scalaIO = IO(println("Welcome to Cats Effect 3"))
  def main(args: Array[String]): Unit = {

Extending with IOApp

Instead of using the default main method, we can use the cats provided IOApp. We need to extend the main app with IOApp and override the run method.

object CatsEffectApp extends IOApp {
    val io: IO[Unit] = IO(println("Welcome to Cats Effect 3"))
    override def run(args: List[String]): IO[ExitCode] = => ExitCode.Success)

Since the run method must return IO[ExitCode], we can map our IO and return the necessary code back.

Extending with IOApp.Simple

Instead of extending the IOApp, we can extend IOApp.Simple. Now we can implement run method. The only difference between run method of IOApp and IOApp.Simple is that run method of IOApp.Simple returns IO[Unit], which is more common in general.

object CatsEffectSimpleApp extends IOApp.Simple {
  val io: IO[Unit] = IO(println("Welcome to Cats Effect 3"))
  override def run: IO[Unit] = io


In this article, we saw how to create a very simple application using Cats Effect. We will look at more features in the next part. The sample code used here is available in GitHub under the package part1.