Janet 1.32.1-cc5beda Documentation
(Other Versions: 1.31.0 1.29.1 1.28.0 1.27.0 1.26.0 1.25.1 1.24.0 1.23.0 1.22.0 1.21.0 1.20.0 1.19.0 1.18.1 1.17.1 1.16.1 1.15.0 1.13.1 1.12.2 1.11.1 1.10.1 1.9.1 1.8.1 1.7.0 1.6.0 1.5.1 1.5.0 1.4.0 1.3.1 )

Fiber Overview

Janet has support for single-core asynchronous programming via coroutines or fibers. Fibers allow a process to stop and resume execution later, essentially enabling multiple returns from a function. This allows many patterns such as schedules, generators, iterators, live debugging, and robust error handling. Janet's error handling is actually built on top of fibers (when an error is thrown, control is returned to the parent fiber).

A temporary return from a fiber is called a yield, and can be invoked with the yield function. To resume a fiber that has been yielded, use the resume function. When resume is called on a fiber, it will only return when that fiber either returns, yields, throws an error, or otherwise emits a signal.

Different from traditional coroutines, Janet's fibers implement a signaling mechanism, which is used to differentiate different kinds of returns. When a fiber yields or throws an error, control is returned to the calling fiber. The parent fiber must then check what kind of state the fiber is in to differentiate errors from return values from user-defined signals.

To create a fiber, use the fiber/new function. The fiber constructor takes one or two arguments. The first argument (required) is the function that the fiber will execute. This function must accept an arity of zero. The next argument (optional) is a collection of flags checking what kinds of signals to trap and return via resume. This is useful so the programmer does not need to handle all of the different kinds of signals from a fiber. Any untrapped signals are simply propagated to the previous calling fiber.

(def f (fiber/new (fn []
                   (yield 1)
                   (yield 2)
                   (yield 3)
                   (yield 4)
                   5)))

# Get the status of the fiber (:alive, :dead, :debug, :new, :pending, or :user0-:user9)
(print (fiber/status f)) # -> :new

(print (resume f)) # -> prints 1
(print (resume f)) # -> prints 2
(print (resume f)) # -> prints 3
(print (resume f)) # -> prints 4
(print (fiber/status f)) # -> :pending
(print (resume f)) # -> prints 5
(print (fiber/status f)) # -> :dead
(print (resume f)) # -> throws an error because the fiber is dead

Using fibers to capture errors

Besides being used as coroutines, fibers can be used to implement error handling (i.e. exceptions).

(defn my-function-that-errors []
 (print "start function")
 (error "oops!")
 (print "never gets here"))

# Use the :e flag to only trap errors.
(def f (fiber/new my-function-that-errors :e))
(def result (resume f))
(if (= (fiber/status f) :error)
 (print "result contains the error")
 (print "result contains the good result"))

Since the above code is rather verbose, Janet provides a try macro which is similar to try/catch in other languages. It wraps the body in a new fiber, resumes the fiber, and then checks the result. If the fiber has errored, an error clause is evaluated.

(try
 (error 1)
 ([err] (print "got error: " err)))
# evaluates to nil and prints "got error: 1"

(try
 (+ 1 2 3)
 ([err] (print "oops")))
# Evaluates to 6 - no error thrown