Vow 🤝📜 — A Data-Oriented Promise/Future Library
×6 faster. ×32 lighter. ×∞ less garbage.Also check out: Scythe - cleanup
What is Vow?
Vow is a zero-allocation, data-oriented Promise/Future library for Luau. It performs the exact same role as evaera’s Promise or RedBlox’s/YetAnotherClown’s Future - allowing you to chain asynchronous operations, wait on resolutions, and handle rejections - but takes a fundamentally different approach to how it stores and resolves them.
Instead of creating an object per scope (a table with a metatable, a dictionary, closure wrappers, etc.), Vow represents each asynchronous operation as a plain integer handle pointing into module-level Structure-of-Arrays (SoA) storage. No objects. No metatables. No per-instance allocation cost.
The result is a library that’s up to 6x faster than traditional Promises, produces zero garbage collector pressure in steady state, and is fully compatible with --!strict typed Luau.
Source: GitHub - Licensed under MPL-2.0.
Why Not Just Use Promise?
Or Future...@evaera’s Promise, RedBlox’s Future, and YetAnotherClown’s Future all follow the same pattern: each promise is an object (a table with a metatable). Every time you .andThen() chain or aggregate with .all(), you’re allocating new tables and wrapping closures. Over hundreds of thousands of operations, this creates a massive graveyard of garbage for the Luau GC to sweep up.
Vow flips this on its head:
| Library | Traditional Promise / Future | Vow |
|---|---|---|
| Scope | Object (table + metatable) | Integer handle (plain number) |
| Data Storage | Dictionaries & Node Trees | Contiguous arrays + buffer |
| Type resolution | Constant OOP lookups | Parallel array lookups |
| Steady-state GC | High (e.g., 1.74 GB over 200k cycles) |
0 KB (Recycled Handles) |
| Strict typing | Varies / Internal casting | Full --!strict Compatibility |
The key insight: Promises don’t need to be objects. Vow pre-allocates SoA arrays and scopes are just integer IDs. Once a Vow is destroyed, its handle is recycled. There is zero heap churn. You get all the power of asynchronous chaining without any of the OOP baggage!
Benchmarks
Overall, Vow leaves a much, much tinier memory footprint and GC pressure while boasting execution speeds that completely demolish traditional OOP Promises. (Up to 32 times lighter!)
Because Vow recycles its integer handles, there is always 0 KB of heap garbage generated in the steady state!
This translates to massively reduced RAM consumption and virtually zero GC micro-stutters. Your game stays smooth, even under intense asynchronous workloads. Yeah, it’s just cool like that. 
Usage
local Vow = require(path.to.Vow)
-- Create a new Vow
local id = Vow.new(function(id)
task.defer(function()
Vow.resolve(id, "ready")
end)
end)
-- Chain with andThen
local nextId = Vow.andThen(id, function(value)
print(value) -- prints: ready
return "finished"
end)
-- Await the result
local ok, value = Vow.await(nextId)
print(value) -- prints: finished
-- IMPORTANT: You must manually destroy Vows to free up slots in the pool
Vow.destroy(id)
Vow.destroy(nextId)
That’s the core workflow. It takes a little while getting used to, but, trust me, it’s worth it.
Strict Typing
Vow is written under --!strict with full type annotations. The module exports a Vow type, so your code stays exceptionally clean:
local Vow = require(path.to.Vow)
local id: Vow.Vow = Vow.new(function(id)
Vow.resolve(id, "Success!")
end)
API Reference
Vow.new(executor) → Vow
Acquire a new Vow and execute the provided function.
Vow.resolve(id, value) → ()
Resolves the Vow with a single value. Can adopt another live Vow.
Vow.reject(id, value) → ()
Rejects the Vow with the given value.
Vow.cancel(id) → ()
Cancels the Vow and flows downwards to its children.
Vow.destroy(id) → ()
Disposes the Vow, detaches it, and recycles the handle.
Vow.andThen(id, handler) → Vow
Chains a callback to run when the Vow resolves.
Vow.catch(id, handler) → Vow
Attaches a rejection callback. Shorthand for andThen(id, nil, onRejected).
Vow.await(id) → (boolean, any)
Yields the current thread until the Vow settles.
Vow.all(vows) → Vow
Returns an aggregate Vow that resolves when all input Vows resolve.
Vow.race(vows) → Vow
Returns an aggregate Vow that settles as soon as any input Vow settles.
Important Notes
Manual Memory Management. Because Vows are opaque integer handles, Luau’s garbage collector cannot automatically discover when a settled Vow is no longer needed. Developers MUST call
Vow.destroy(id)when completely finished with a Vow to release it back into the pool. Using a handle after destruction is undefined behavior. Treatdestroy()as final!Pro tip: you can pair Vow with Scythe - a data-oriented cleanup library! (update coming soon)
local MyVow = Vow.new(function(id)
print("sup")
end)
local MyScythe = Scythe.scope()
Scythe.add(MyScythe, function()
print("cleaned")
Vow.destroy(MyVow)
end)
That’s all, for now.
Get It
- Source: GitHub
- License: MPL-2.0 (open source, use it freely)
- Installation:
Vow is available on Wally! You can get it by modifying yourwally.toml, like so:
[dependencies]
Vow = "synttx/vow@1.0.0"
Alternatively, you can download the latest release or clone the source directly into your project.
If you have questions, suggestions, or find a bug - drop a reply below or open an issue on GitHub. Contributions are welcome.
Vow also has a Roblox OSS Community Discord thread!
Support Me
If you like Vow or Scythe or all other libraries to come (and there’s a bunch!), or you appreciate what I do - you can support me by following me on Roblox or/and starring my GitHub OSS monorepository.
Thank you!