In most use cases, Godot's signal handlers are enough for any asynchronous task that needs to be done in reaction to something. However, signal syntax can be quite elaborate and verbose in more complex situations and can be confusing to people new to Godot. Also, signals can't do any error handling by themselves - they're just simple data busses, which might not be ideal for some use cases.
JS-like Promise-based approach presents a more high-level, action-based approach to asynchronous processing, while also presenting a way to handle any errors in case something went wrong in the Promise.
This addon is an implementation of this functionality in GDScript, allowing async operations in a simpler way, familiar to many people.
In order to make it possible for Promises to keep their values after resolving/rejecting, a new recommended way for awaiting Promises was created.
While this will still work for existing projects, awaiting for settled
signal is now deprecated and should be replaced with wait()
(see Using await and Promise.PromiseResult
section below)
- Copy
addons/promise/
to youraddons
directory. - It's done!
Like in JS, Promise can be created by instantiating a new object with a Callable passed.
var promise := Promise.new(
func(resolve: Callable, reject: Callable):
# put your code here and call resolve.call() or reject.call() at some point
pass
)
resolve
callable can be called without any arguments - resolved value will be treated as null
in that case.
When Promise is created, passed Callable will be called in a deferred mode, i.e. it will be executed at the end of a current frame. This allows different callbacks to get connected properly regardless if Callable is run asynchronously or not.
There are three ways to handle success or failure:
Similar to original Promises, you can add resolve/reject callbacks via then()
and catch()
. This will not pause the execution of the code.
Promise.new(
func(resolve: Callable, reject: Callable):
await get_tree().create_timer(1.0).timeout
if randi_range(0, 10) > 5:
# resolve() expects Variant value, so any type can be passed
resolve.call("I'm bigger than 5!")
else:
# This implementation's error handling is done via Promise.Rejection class.
# It's a simple data wrapper that contains `reason: String` and `stack: Array` (This is a standard get_stack() trace that won't work in non-debug builds).
# You can always extend it to give more meaning type-wise.
reject.call(Promise.Rejection.new("Lesser or equal to 5"))
).then(
func(value: String):
print(value)
).catch(
func(rejection: Promise.Rejection):
print(rejection.reason)
)
At this moment, chaining multiple then()
calls with Callables that return next Promise in the sequence is not supported.
This effectively means that Promise doesn't behave the same as in JS, as the value returned by the callback in then()
is completely ignored.
However, this kind of sequentiality can be worked around by using either nested then()
calls or by using await
keyword as described in the section below.
If you want to wait until Promise settles (i.e. when Promise is either resolved or rejected), all you need to do is to await
on a wait()
function added at the end of the function chain.
Modifying previous example:
var result := await Promise.new(
func(resolve: Callable, reject: Callable):
await get_tree().create_timer(1.0).timeout
if randi_range(0, 10) > 5:
resolve.call("I'm bigger than 5!")
else:
reject.call(Promise.Rejection.new("Lesser or equal to 5"))
).wait()
prints(result.status, result.payload)
result
contains status: enum Promise.Status
(either Promise.Status.RESOLVED
or Promise.Status.REJECTED
) and payload: Variant
(the value passed to resolve()
or Promise.Rejection
object).
Since GDScript doesn't have dedicated try-catch functionality, you can handle the result in if statements after the Promise settles:
var result := await Promise.new(...).wait()
if result.status == Promise.Status.RESOLVED:
# do stuff
else:
# do error handling
To address the issue with sequential chaining of then()
calls, you can wait for the returned Promise when the first Promise resolves and then use the Promise from the returned result:
var first_result := await Promise.new(
func(resolve: Callable, reject: Callable):
await get_tree().create_timer(1.0).timeout
resolve.call(Promise.new(
func(inner_resolve: Callable, inner_reject: Callable):
# some other Promise...
))
).wait()
if first_result.status == Promise.Status.RESOLVED:
var second_result := await result1.payload.wait()
# handle the result
Also, it is worth noting that you can await for settled
, resolved
and rejected
signals specifically (but you should use wait()
unless you know what you're doing).
In case you want to handle Promises in a functional way, but still retain the ability to await
for their settlement,
you can just put wait()
after setting then()
and catch()
handlers.
var promise := Promise.new(
func(resolve: Callable, reject: Callable):
await get_tree().create_timer(1.0).timeout
resolve.call("slow")
)
await promise.then(
func(value: String):
prints("Value: ", value)
).catch(
func(rejection: Promise.Rejection):
push_error(rejection.reason)
).wait()
# execution will pause until the Promise is settled and one of the passed callbacks is called.
This library also provides the implementation of Promise.all()
, Promise.any()
, Promise.resolve()
and Promise.reject()
.
This static method returns a Promise that resolves when all passed Promises resolve and rejects when at least one of them rejects.
var promise1 := Promise.new(
func(resolve: Callable, reject: Callable):
await get_tree().create_timer(1.0).timeout
resolve.call("slow")
)
var promise2 := Promise.new(
func(resolve: Callable, reject: Callable):
await get_tree().create_timer(0.1).timeout
resolve.call("quick")
)
var result := await Promise.all([promise1, promise2]).wait()
if result.status == Promise.Status.RESOLVED:
print(result.payload) # ["slow", "quick"]
var promise1 := Promise.new(
func(resolve: Callable, reject: Callable):
await get_tree().create_timer(1.0).timeout
reject.call(Promise.Rejection.new("Failed!"))
)
var promise2 := Promise.new(
func(resolve: Callable, reject: Callable):
await get_tree().create_timer(0.1).timeout
resolve.call("quick")
)
var result := await Promise.all([promise1, promise2]).wait()
if result.status == Promise.Status.REJECTED:
print(result.payload.reason) # "Failed!" - the Promise.Rejection that was thrown is passed
This static method returns a Promise that resolves when any of passed Promises resolves and rejects when all of them reject.
When one of them resolves:
var promise1 := Promise.new(
func(resolve: Callable, reject: Callable):
await get_tree().create_timer(1.0).timeout
resolve.call("slow")
)
var promise2 := Promise.new(
func(resolve: Callable, reject: Callable):
await get_tree().create_timer(0.1).timeout
resolve.call("quick")
)
var result := await Promise.any([promise1, promise2]).wait()
if result.status == Promise.Status.RESOLVED:
print(result.payload) # "quick"
When all were rejected:
var promise1 := Promise.new(
func(resolve: Callable, reject: Callable):
await get_tree().create_timer(1.0).timeout
reject.call(Promise.Rejection.new("Nope"))
)
var promise2 := Promise.new(
func(resolve: Callable, reject: Callable):
await get_tree().create_timer(0.1).timeout
resolve.call(Promise.Rejection.new("Nope, but quicker"))
)
var result := await Promise.any([promise1, promise2]).wait()
if result.status == Promise.Status.REJECTED:
# Promise.any() rejects with a special Promise.Rejection type: PromiseAnyRejection
# It contains group property which stores all rejections.
print(result.payload.group) # prints the array with rejections "Nope" and "Nope, but quicker"
This static method allows creation of a Promise that will immediately resolve to a specified value.
var result = await Promise.resolve("Hello!").wait()
print(result.payload) # "Hello!"
This static method allows creation of a Promise that will immediately reject with a specified Promise.Rejection
.
var result = await Promise.reject(Promise.Rejection.new("Error!")).wait()
print(result.payload.reason) # "Error!"
Promise class contains small utilities to convert signals into Promises.
Using from()
and from_many()
, you can convert a signal or array of signals into a Promise and array of Promises, respectively.
signal value_changed(value: Variant)
var result := await Promise.from(value_changed).wait()
# result.payload is the first value emitted by signal
signal first_signal(value: Variant)
signal second_signal(value: Variant)
var result := await Promise.all(Promise.from_many([first_signal, second_signal])).wait()
# result.payload is an array of resolved values, in order of signals in the input array.
Note: promisified signals will never reject, since they do not implement any error handling by design. Note 2: If signal has more arguments than one, the resolved value will be an array of values. Also, signals without any arguments will resolve to null.
This library uses gdUnit4 as the unit test framework, but it is not provided in this repo. In order to run the tests, it needs to be installed manually (more information here: https://mikeschulze.github.io/gdUnit4/first_steps/install/). After that, tests can be run from the editor.
Distributed under the MIT License.