Promises.jl

Use JavaScript Promises syntax in Julia! (alpha)
Author fonsp
Popularity
25 Stars
Updated Last
10 Months Ago
Started In
March 2022

Promises.jl: JavaScript-inspired async

Summary:

A Promise{T} is a container for a value that will arrive in the future.

You can await Promises, and you can chain processing steps with .then and .catch, each producing a new Promise.

Let's look at an example, using Promises.jl to download data in the background:

download_result = @async_promise begin

	# This will download the data, 
	#  write the result to a file, 
	#  and return the filename.
	Downloads.download("https://api.github.com/users/$(username)")
end

#=>  Promise{Any}( <pending> )
username = "JuliaLang"

The result is a pending promise: it might still running in the background!

download_result

#=>  Promise{Any}( <pending> )

You can use @await to wait for it to finish, and get its value:

@await download_result

#=>  "/var/folders/v_/fhpj9jn151d4p9c2fdw2gv780000gn/T/jl_LqoUCC"

Chaining with then

One cool feature of promises is chaining! Every promise has a then function, which can be used to add a new transformation to the chain, returning a new Promise.

download_result.then(
	filename -> read(filename, String)
).then(
	str -> JSON.parse(str)
)

#=>  
Promise{Dict{String, Any}}( <resolved>: Dict{String, Any} with 32 entries:
  "followers"         => 0
  "created_at"        => "2011-04-21T06:33:51Z"
  "repos_url"         => "https://api.github.com/users/JuliaLang/repos"
  "login"             => "JuliaLang"
  "gists_url"         => "https://api.github.com/users/JuliaLang/gists{/gist_id}"
  "public_repos"      => 36
  "following"         => 0
  "site_admin"        => false
  "name"              => "The Julia Programming Language"
  "location"          => nothing
  "blog"              => "https://julialang.org"
  "subscriptions_url" => "https://api.github.com/users/JuliaLang/subscriptions"
  "id"                => 743164
  ⋮                   => ⋮ )

Since the original Promise download_result was asynchronous, this newly created Promise is also asynchronous! By chaining the operations read and JSON.parse, you are "queing" them to run in the background.


Error handling: rejected Promises

A Promise can finish in two ways: it can ✓ resolve or it can ✗ reject. In both cases, the Promise{T} will store a value, either the resolved value (of type T) or the rejected value (often an error message).

When an error happens inside a Promise handler, it will reject:

bad_result = download_result.then(d -> sqrt(-1))

#=>  
Promise{Any}( <rejected>: 
DomainError with -1.0:
sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
 [1] throw_complex_domainerror(f::Symbol, x::Float64)
   @ Base.Math ./math.jl:33
 [2] sqrt
   @ ./math.jl:567 [inlined]
 [3] sqrt(x::Int64)
   @ Base.Math ./math.jl:1221
 [4] (::Main.var"#5#6"{typeof(sqrt)})(d::String)
   @ Main ~/Documents/Promises.jl/src/notebook.jl#==#34364f4d-e257-4c22-84ee-d8786a2c377c:1
 [5] promise_then(p::Promise{Any}, f::Main.var"#5#6"{typeof(sqrt)})
   @ Main.workspace#3 ~/Documents/Promises.jl/src/notebook.jl#==#49a8beb7-6a97-4c46-872e-e89822108f39:63
 [6] #18
   @ ~/Documents/Promises.jl/src/notebook.jl#==#49a8beb7-6a97-4c46-872e-e89822108f39:175 [inlined]
 )

If you @await a Promise that has rejected, the rejected value will be rethrown as an error:

@await bad_result

#=>  
DomainError with -1.0:
sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
 [1] throw_complex_domainerror(f::Symbol, x::Float64)
   @ Base.Math ./math.jl:33
 [2] sqrt
   @ ./math.jl:567 [inlined]
 [3] sqrt(x::Int64)
   @ Base.Math ./math.jl:1221
 [4] (::var"#5#6"{typeof(sqrt)})(d::String)
   @ Main ~/Documents/Promises.jl/src/notebook.jl#==#34364f4d-e257-4c22-84ee-d8786a2c377c:1
 [5] promise_then(p::Main.workspace#3.Promise{Any}, f::var"#5#6"{typeof(sqrt)})
   @ Main.workspace#3 ~/Documents/Promises.jl/src/notebook.jl#==#49a8beb7-6a97-4c46-872e-e89822108f39:63
 [6] #18
   @ ~/Documents/Promises.jl/src/notebook.jl#==#49a8beb7-6a97-4c46-872e-e89822108f39:175 [inlined]
Stacktrace:
 [1] fetch(p::Main.workspace#3.Promise{Any})
   @ Main.workspace#3 ~/Documents/Promises.jl/src/notebook.jl#==#49a8beb7-6a97-4c46-872e-e89822108f39:112

The Promise constructor

Remember that a promise can finish in two ways: it can ✓ resolve or it can ✗ reject. When creating a Promise by hand, this corresponds to the two functions passed in by the constructor, resolve and reject:

Promise{T=Any}(resolve, reject) -> begin

	if condition
		# Resolve the promise:
		resolve("Success!")
	else
		# Reject the promise
		reject("Something went wrong...")
	end
end)
yay_result = Promise((resolve, reject) -> resolve("🌟 yay!"))

#=>  Promise{Any}( <resolved>: "🌟 yay!" )
oopsie_result = Promise((res, rej) -> rej("oops!"))

#=>  Promise{Any}( <rejected>: "oops!" )

(A shorthand function is available to create promises that immediately reject or resolve, like we did above: Promises.resolve(value) and Promises.reject(value).)


Chaining errors with .catch

There are two special things about rejected values in chains:

  1. The .then function of a rejected Promise will immediately reject, passing the value along.
Promise((res, rej) -> rej("oops!")).then(x -> x + 10).then(x -> x / 100)

#=>  Promise{Any}( <rejected>: "oops!" )
  1. The .catch is the opposite of .then: it is used to handle rejected values.
Promise((res, rej) -> rej("oops!")).then(x -> x + 10).catch(x -> 123)

#=>  Promise{Any}( <resolved>: 123 )

Here is a little table:

.then .catch
On a resolved Promise: Runs Skipped
On a rejected Promise: Skipped Runs

Promise{T} is a parametric type

Like in TypeScript, the Promise{T} can specify its resolve type. For example, Promise{String} is guaranteed to resolve to a String.

Promise{String}((res,rej) -> res("asdf"))

#=>  Promise{String}( <resolved>: "asdf" )

This information is available to the Julia compiler, which means that it can do smart stuff!

Core.Compiler.return_type(fetch, (Promise{String},))

#=>  String

Trying to resolve to another type will reject the Promise:

Promise{String}((res,rej) -> res(12341234))

#=>  
Promise{String}( <rejected>: 
ArgumentError: Can only resolve with values of type String.
Stacktrace:
 [1] (::Main.workspace#3.var"#resolve#20"{String, Promise{String}})(val::Int64)
   @ Main.workspace#3 ~/Documents/Promises.jl/src/notebook.jl#==#49a8beb7-6a97-4c46-872e-e89822108f39:21
 [2] (::Main.var"#25#26")(res::Main.workspace#3.var"#resolve#20"{String, Promise{String}}, rej::Function)
   @ Main ~/Documents/Promises.jl/src/notebook.jl#==#9d9179de-19b1-4f40-b816-454a8c071c3d:1
 [3] Promise{String}(f::Main.var"#25#26")
   @ Main.workspace#3 ~/Documents/Promises.jl/src/notebook.jl#==#49a8beb7-6a97-4c46-872e-e89822108f39:38
 )

Automatic types

Julia is smart, and it can automatically determine the type of chained Promises using static analysis!

typeof(
	Promise{String}((res,rej) -> res("asdf")).then(first)
)

#=>  Promise{Char}

Used By Packages

No packages found.