I Vow you you'll switch from Promise/Future after reading this post

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

Check out the benchmarks section on GitHub for a detailed benchmark overview, memory footprint graphs, and more.

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. :eyes:


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

:police_car_light: 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. Treat destroy() 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 your wally.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
Hey, psst... Got a minute?

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!

21 Likes

Loving it, moved from future and i do not regret it :eyes:

The next big move will be abandoning the bloat altogether and writing plain, sane luau :innocent:

1 Like