Skip to content

Roblox Lua Style Guide

This style guide aims to unify as much Lua code at Roblox as possible under the same style and conventions.

This guide is designed after Google's C++ Style Guide. Although Lua is a significantly different language, the guide's principles still hold.

Guiding Principles

  • Optimize code for reading, not writing.
  • Avoid surprising or dangerous Lua features:
    • Metatables are a good example of a powerful feature that should be used with care.
  • Be consistent with idiomatic Lua when appropriate.

File Structure

Files should consist of these things (if present) in order:

  1. An optional block comment talking about why this file exists
    • Don't attach the file name, author, or date -- these are things that our version control system can tell us.
  2. Services used by the file, using GetService
  3. Module imports, using require
  4. Module-level constants
  5. Module-level variables and functions
  6. The object the module returns
  7. A return statement!

Requires

  • All require calls should be at the top of a file, making dependencies static.
  • Use relative paths when importing modules from the same package.

    local OtherThing = require(script.Parent.OtherThing)
    
  • Use absolute paths when importing modules from a different package.

    local CorePackages = game:GetService("CorePackages")
    local Roact = require(CorePackages.Roact)
    

Metatables

Metatables are an incredibly powerful Lua feature that can be used to overload operators, implement prototypical inheritance, and tinker with limited object lifecycle.

At Roblox, we limit use of metatables to a couple cases:

  • Implementing prototype-based classes
  • Guarding against typos

Prototype-based classes

The most popular pattern for classes in Lua is sometimes referred to as the One True Pattern. It defines class members, instance members, and metamethods in the same table and highlights Lua's strengths well.

First up, we create a regular, empty table:

local MyClass = {}

Next, we assign the __index member on the class back to itself. This is a handy trick that lets us use the class's table as the metatable for instances as well.

When we construct an instance, we'll tell Lua to use our __index value to find values that are missing in our instances. It's sort of like prototype in JavaScript, if you're familiar.

MyClass.__index = MyClass

In most cases, we create a default constructor for our class. By convention, we usually call it new.

Methods that don't operate on instances of our class are usually defined using a dot (.) instead of a semicolon (:).

function MyClass.new()
    local self = {
        -- Define members of the instance here, even if they're `nil` by default.
        phrase = "bark",
    }

    -- Tell Lua to fall back to looking in MyClass.__index for missing fields.
    setmetatable(self, MyClass)

    return self
end

We can also define methods that operate on instances. These are just methods that expect their first argument to be an instance. By convention, we define them using a colon (:):

-- This is functionally identical to `function MyClass.bark(self)`
function MyClass:bark()
    print("My phrase is", self.phrase)
end

At this point, our class is ready to use!

We can construct instances and start tinkering with it:

local instance = MyClass.new()

-- Properties on the instance are visible, since it's just a table:
print(instance.phrase) -- "bark"

-- Methods are pulled from MyClass because of our metatable:
instance:bark() -- "My phrase is bark"

-- We can also invoke methods with a dot by explicitly passing `instance`:
MyClass.bark(instance)
instance.bark(instance)

Further additions you can make to your class as needed:

  • Introduce a __tostring metamethod to make debugging easier
  • Define quasi-private members using two underscores as a prefix
  • Add a method to check type given an instance, like:

    function MyClass.isMyClass(instance)
        return getmetatable(instance).__index == MyClass
    end
    

Guarding against typos

Indexing into a table in Lua gives you nil if the key isn't present, which can cause errors that are difficult to trace!

Our other major use case for metatables is to prevent certain forms of this problem. For types that act like enums, we can carefully apply an __index metamethod that throws:

local MyEnum = {
    A = "A",
    B = "B",
    C = "C",
}

setmetatable(MyEnum, {
    __index = function(self, key)
        error(string.format("%q is not a valid member of MyEnum",
            tostring(key)), 2)
    end,
})

Since __index is only called when a key is missing in the table, MyEnum.A and MyEnum.B will still give you back the expected values, but MyEnum.FROB will throw, hopefully helping engineers track down bugs more easily.

General Whitespace

  • Keep lines under 120 columns wide, assuming four column wide tabs.
    • Luacheck will warn for lines over 120 bytes wide; it isn't accurate with tab characters!
  • Wrap comments to 80 columns wide, assuming four column wide tabs.
    • This is different than normal code; the hope is that short lines help improve readability of comment prose, but is too restrictive for code.
  • Indent with tabs.
  • Don't leave whitespace at the end of lines.
    • If your editor has an auto-trimming function, turn it on!
  • No vertical alignment!

    • Vertical alignment makes code more difficult to edit and often gets messed up by subsequent editors.

    Good:

    local frobulator = 132
    local grog = 17
    

    Bad:

    local frobulator = 132
    local grog       =  17
    

  • Use a single empty line to express groups when useful. Do not start blocks with a blank line. Excess empty lines harm whole-file readability.

    local Foo = require(Common.Foo)
    
    local function gargle()
        -- gargle gargle
    end
    
    Foo.frobulate()
    Foo.frobulate()
    
    Foo.munge()
    
  • Use one statement per line. Prefer to put function bodies on new lines.

    Good:

    table.sort(stuff, function(a, b)
        local sum = a + b
        return math.abs(sum) > 2
    end)
    

    Bad:

    table.sort(stuff, function(a, b) local sum = a + b return math.abs(sum) > 2 end)
    

  • Put a space before and after operators, except when clarifying precedence.

    Good:

    print(5 + 5 * 6^2)
    

    Bad:

    print(5+5* 6 ^2)
    

  • Put a space after each commas in tables and function calls.

    Good:

    local friends = {"bob", "amy", "joe"}
    foo(5, 6, 7)
    

    Bad:

    local friends = {"bob","amy" ,"joe"}
    foo(5,6 ,7)
    

  • When creating blocks, inline any opening syntax elements.

    Good:

    local foo = {
        bar = 2,
    }
    
    if foo then
        -- do something
    end
    

    Bad:

    local foo =
    {
        bar = 2,
    }
    
    if foo
    then
        -- do something
    end
    

Blocks

  • Don't use parentheses around the conditions in if, while, or repeat blocks. They aren't necessary in Lua!

    if CONDITION then
    end
    
    while CONDITION do
    end
    
    repeat
    until CONDITION
    
  • Use do blocks if limiting the scope of a variable is useful.

    local getId
    do
        local lastId = 0
        getId = function()
            lastId = lastId + 1
            return lastId
        end
    end
    

Literals

  • Use double quotes when declaring string literals.

    • Using single quotes means we have to escape apostrophes, which are often useful in English words.
    • Empty strings are easier to identify with double quotes, because in some fonts two single quotes might look like a single double quote ("" vs '')

    Good:

    print("Here's a message!")
    

    Bad:

    print('Here\'s a message!')
    

Tables

  • Avoid tables with both list-like and dictionary-like keys.
    • Iterating over these mixed tables is troublesome.
  • Iterate over list-like tables with ipairs and dictionary-like tables with pairs.
    • This helps clarify what kind of table we're expecting in a given block of code!
  • Add trailing commas in multi-line tables.

    • This lets us re-sort lines with a single keypress and makes diffs cleaner when adding new items.
    local frobs = {
        andrew = true,
        billy = true,
        caroline = true,
    }
    
  • Avoid putting curly braces for tables on their own line. Doing so harms readability, since it forces the reader to move to another line in an awkward spot in the statement.

    Good:

    local foo = {
        bar = {
            baz = "baz",
        },
    }
    
    frob({
        x = 1,
    })
    

    Bad:

    local foo =
    {
        bar =
    
        {
            baz = "baz",
        },
    }
    
    frob(
    {
        x = 1,
    })
    

    Exception:

    -- In function calls with large inline tables or functions, sometimes it's
    -- more clear to put braces and functions on new lines:
    foo(
        {
            type = "foo",
        },
        function(something)
            print("Hello," something)
        end
    )
    
    -- As opposed to:
    foo({
        type = "foo",
    }, function(something) -- How do we indent this line?
        print("Hello,", something)
    end)
    

  • Break dictionary-like tables with more than a couple keys onto multiple lines.

    Good:

    local foo = { type = "foo" }
    
    local bar = {
        type = "bar",
        phrase = "hooray",
    }
    
    -- It's also okay to use multiple lines for a single field
    local baz = {
        type = "baz",
    }
    

    Bad:

    local stuff = { hello = "world", hola = "mundo", howdy = "y'all", sup = "homies" }
    

  • Break list-like tables onto multiple lines however it makes sense.

    • Make sure to follow the line length limit!
    local libs = { "roact", "rodux", "testez", "cryo", "otter" }
    
    -- You can break these onto multiple lines, which makes diffs cleaner:
    local libs = {
        "roact",
        "rodux",
        "testez",
        "cryo",
        "otter",
    }
    
    -- We can also group them, if grouping has useful information:
    local libs = {
        "roact", "rodux", "cryo",
    
        "testez", "otter",
    }
    

Functions

  • Keep the number of arguments to a given function small, preferably 1 or 2.
  • Declare named functions using function-prefix syntax. Non-member functions should always be local.

    Good:

    local function add(a, b)
        return a + b
    end
    

    Bad:

    -- This is a global!
    function add(a, b)
        return a + b
    end
    
    local add = function(a, b)
        return a + b
    end
    

    Exception:

    -- An exception can be made for late-initializing functions in conditionals:
    local doSomething
    
    if CONDITION then
        function doSomething()
            -- Version of doSomething with CONDITION enabled
        end
    else
        function doSomething()
            -- Version of doSomething with CONDITION disabled
        end
    end
    

  • When declaring a function inside a table, use function-prefix syntax. Differentiate between . and : to denote intended calling convention.

    Good:

    -- This function should be called as Frobulator.new()
    function Frobulator.new()
        return {}
    end
    
    -- This function should be called as Frobulator:frob()
    function Frobulator:frob()
        print("Frobbing", self)
    end
    

    Bad:

    function Frobulator.garb(self)
        print("Frobbing", self)
    end
    
    Frobulator.jarp = function()
        return {}
    end
    

Comments

  • Wrap comments to 80 columns wide.
    • It's easier to read comments with shorter lines, but fitting code into 80 columns can be challenging.
  • Use single line comments for inline notes:

    • If the comment spans multiple lines, use multiple single-line comments.
    • Sublime Text has an automatic wrap feature (alt+Q on Windows) to help with this!
    -- This condition is really important because the world would blow up if it
    -- were missing.
    if not foo then
        stopWorldFromBlowingUp()
    end
    
  • Use block comments for documenting items:

    • Use a block comment at the top of files to describe their purpose.
    • Use a block comment before functions or objects to describe their intent.
    --[[
        Shuts off the cosmic moon ray immediately.
    
        Should only be called within 15 minutes of midnight Mountain Standard
        Time, or the cosmic moon ray may be damaged.
    ]]
    local function stopCosmicMoonRay()
    end
    
  • Comments should focus on why code is written a certain way instead of what the code is doing.

    Good:

    -- Without this condition, the aircraft hangar would fill up with water.
    if waterLevelTooHigh() then
        drainHangar()
    end
    

    Bad:

    -- Check if the water level is too high.
    if waterLevelTooHigh() then
        -- Drain the hangar
        drainHangar()
    end
    

  • No section comments!

Naming

  • Spell out words fully! Abbreviations generally make code easier to write, but harder to read.
  • Use PascalCase names for class and enum-like objects.
  • Use camelCase names for local variables, member values, and functions.
  • Use LOUD_SNAKE_CASE names for local consants.
  • Prefix private members with an underscore, like _camelCase.
    • Lua does not have visibility rules, but using a character like an underscore helps make private access stand out.
  • A File's name should match the name of the object it exports.
    • If your module exports a single function named doSomething, the file should be named doSomething.lua.

FooThing.lua:

local FOO_THRESHOLD = 6

local FooThing = {}

FooThing.someMemberConstant = 5

function FooThing.go()
    print("Foo Delta:", FooThing.someMemberConstant - FOO_THRESHOLD)
end

return FooThing

Yielding

Do not call yielding functions on the main task. Wrap them in coroutine.wrap or delay, and consider exposing a Promise or Promise-like async interface for your own functions.

Pros:

  • Roblox's yielding model makes calling asynchronous tasks transparent to the user, which lets users call complicated functions without understanding coroutines or other async primitives.

Cons:

  • Unintended yielding can cause hard-to-track data races. Simple code involving callbacks can cause confusing bugs if the input callback yields.

    local value = 0
    
    local function doSomething(callback)
        local newValue = value + 1
        callback(newValue)
        value = newValue
    end
    

Error Handling

When writing functions that can fail, return success, result, use a Result type, or use an async primitive that encodes failure, like Promise.

Do not throw errors except when validating correct usage of a function.

local function thisCanFail(someValue)
    assert(typeof(someValue) == "string", "someValue must be a string!")

    if success() then
        return true, "Congratulations! You won!"
    else
        return false, Error.new("ERR_BLAH", "Something horrible failed!")
    end
end

Pros:

  • Using exceptions lets unhandled errors bubble up 'automatically' to your caller
  • Stack traces are automatically attached to errors

Cons:

  • Lua can only throw strings as errors, which makes distinguishing between them very difficult
  • Exceptions are not encoded into a function's contract explicitly. By returning success, result, you force your caller to consider whether an error will happen.

Exceptions:

  • When calling functions that communicate failure by throwing, wrap calls in pcall and make it clear via comment what kinds of errors you're expecting to handle.

General Roblox Best Pratices

  • All services should be referenced using game:GetService at the top of the file.
  • When importing a module, use the name of the module for its variable name.