Build an effect, run nothing: that is the part that trips up newcomers. This pipeline is a complete description of some work, an Effect of number that has not actually happened yet.
To get a value out, you run it at the edge of your program. For synchronous work, Effect.runSync executes the description and hands you back a plain number, no Effect wrapper in sight.
When the work is asynchronous you reach for runPromise instead, and await the promise it returns, the same idea at an async edge.
Now the trap that caught me. Calling runSync on an effect that fails will throw, the exact thing we came here to escape. The safe door is runSyncExit, which never throws and instead hands you an Exit you can calmly inspect. So run only at the edges, and keep the whole middle of your program pure.