Appearance
Diagnosing Errors
Written: 2026-10-14
Learning objectives
- You read expected-versus-got errors.
- You fix the top eight strict patterns fast.
- You match return arity on every path.
- You break nilable chains with guards.
- You stop sealed growth and any leaks.
- You shrink failures to minimal repros.
Prerequisites
You read the mismatch, not the red
Every diagnostic says what the slot wanted and what you supplied. You find the slot first.
The precise rule is expected versus got. The checker names the wanted type at a param, return, assignment, or field. It names the supplied type you sent. Conversion failures like string into number are documented in type checking. The luau.org types intro frames the same mismatches as inference plus annotation failures.
Studio can phrase the same mismatch differently across releases. These chapters describe meaning and quote exact strings only when taken from fetched docs. You match on types and locations, not on exact prose.
You read the first error in the file. You jump to its line. You fix that slot. You recheck before touching later red.
Put this Script in ServerScriptService.
luau
--!strict
local function pay(coins: number): number
return coins * 2
end
local got = pay("5")This block errors. The slot wants number. You supply string. The meaning matches the documented string-into-number failure. You fix it by passing 5 or parsing the text with tonumber plus a nil guard. You infer got as number after the fix.
Eight patterns cover most red
Strict errors repeat. You learn the eight shapes and their fixes.
The precise rule is one fix per shape. Each pattern below shows a wrong sample that errors, then a fix that checks clean. Concepts point back to Annotations and Inference, Narrowing and Guards, Casts and Assertions, and Solver and Migration.
The limitation is masking. One root can paint many lines. Suppression trims cascades per type error suppression. You always fix the earliest line first.
You stop reading every diagnostic. You classify, fix, and recheck. Eight shapes become reflex.
Put this Script in ServerScriptService.
luau
--!strict
local rate: number = "fast"This wrong pattern errors. A string cannot convert into a number. You fix it with a true number.
luau
--!strict
local rate: number = 4This fix checks clean. You infer rate as number.
Put this Script in ServerScriptService.
luau
--!strict
export type Sword = { name: string, damage: number }
local blade: Sword = { name = "Dune" }This wrong pattern errors. A required prop is missing. You fix it by adding the field.
luau
--!strict
export type Sword = { name: string, damage: number }
local blade: Sword = { name = "Dune", damage = 7 }This fix checks clean. You infer blade as Sword.
Put this Script in ServerScriptService.
luau
--!strict
local function hi(name: string?): string
return name
endThis wrong pattern errors. The return may be nil but the signature promises string. You fix it with a guard or an optional return.
luau
--!strict
local function hi(name: string?): string
if name ~= nil then
return name
end
return "guest"
endThis fix checks clean. You infer hi as (string?) -> string. The guard proves presence on one path. The fallback covers absence.
Put this Script in ServerScriptService.
luau
--!strict
local function pair(): (number, string)
return 4
endThis wrong pattern errors. The signature wants two values but the path sends one. You fix arity on every path.
luau
--!strict
local function pair(): (number, string)
return 4, "gem"
endThis fix checks clean. Both values match the declared pack.
Put this Script in ServerScriptService.
luau
--!strict
export type Pet = { name: string }
local pet: Pet = { name = "Pip" }
pet.hunger = 1This wrong pattern errors. The annotation seals the table, so growth fails. You fix it by extending the alias or building unsealed first.
luau
--!strict
local pet = { name = "Pip" }
pet.hunger = 1This fix checks clean. You infer pet as { name: string, hunger: number }. The literal starts unsealed per unsealed literals.
Put this Script in ServerScriptService.
luau
--!strict
local bag: any = { coins = 3 }
local total = bag.coins + "bonus"This wrong pattern may pass the checker while failing at runtime. any suppresses the check. You fix it with a precise record.
luau
--!strict
export type Bag = { coins: number }
local bag: Bag = { coins = 3 }
local total = bag.coins + 4This fix checks clean. You infer total as number. Misuse now errors in the editor.
Put this Script in ServerScriptService.
luau
--!strict
local count: number = 5
local name = count :: stringThis wrong pattern errors. The pair is unrelated, so bidirectional casting rejects it per syntax type ascription bidi. You fix it by remodeling.
luau
--!strict
local count: number = 5
local name = tostring(count)This fix checks clean. You infer name as string.
Put this Script in ServerScriptService.
luau
--!strict
local function first<T>(list: { T }): T?
return list[1]
end
local got = first({ 1, "two" })This wrong pattern infers a wider member than callers expect. Mixed literals widen T. Later numeric math on got errors. You fix it with uniform lists or an explicit union element type.
luau
--!strict
local function firstN(list: { number }): number?
return list[1]
end
local got = firstN({ 1, 2 })This fix checks clean. You infer got as number?. Uniform input keeps later math safe.
Arity mismatches hide in return paths
The signature counts outputs. Each return must send that count with those types.
The precise rule is all paths match. A : (string, number) function sends two values everywhere. A : number function sends one number everywhere. A : () function sends nothing. Multi-return syntax is defined in type checking and the syntax tour.
You duplicate a two-return helper, change one path, and forget the other. Pasted (ok, value) pairs are the classic source.
You search each return after editing a signature. You bind both values at callers that need both.
Put this Script in ServerScriptService.
luau
--!strict
local function find(full: string): (string, string)
local at = string.find(full, " ")
if at == nil then
return full
end
return string.sub(full, 1, at - 1), string.sub(full, at + 1)
endThis block errors. The early path sends one value where two are declared. You fix it by returning full, "" on that path. After the fix you infer both results as string.
Put this Script in ServerScriptService.
luau
--!strict
local function find(full: string): (string, string)
local at = string.find(full, " ")
if at == nil then
return full, ""
end
return string.sub(full, 1, at - 1), string.sub(full, at + 1)
end
local a, b = find("Sky Fox")This block checks clean. You infer a as string and b as string. Both paths send the full pack.
Nilable chains break with one guard each
a may hold b, and b may hold c. Each hop can be absent. You prove each hop.
The precise rule is guard per hop. A chain like player to character to head only checks when each step refines to present. One guard at the end cannot prove the middle. Optionals and FindFirstChild nilability follow Narrowing and Guards.
Deep chains indent fast. Long chains also hide which hop failed. You flatten with early returns.
You return on the first nil. Happy-path code stays shallow. Each guard names the missing piece.
Put this Script in ServerScriptService.
luau
--!strict
local function headName(model: Model?): string
if model ~= nil then
local head = model:FindFirstChild("Head")
if head ~= nil then
return head.Name
end
end
return "none"
end
local dummy = Instance.new("Model")
local got = headName(dummy)This block checks clean. You infer got as string. The outer guard proves model. The inner guard proves head. A direct model:FindFirstChild("Head").Name without guards errors on both nil cases.
Sealed growth and any leaks have one cure each
Sealed growth means you built too late. Any leaks mean you typed too little.
The precise rule is build-then-seal and parse-at-edge. You finish unsealed construction before annotating or returning. Table states and width are defined in table types and sealed subtyping. You replace any with records plus guards at package edges per Casts and Assertions.
Old code grows shared tables across functions. Old packages return any. Both need small refactors, not one-line patches.
Construction helpers return complete literals. Parsers turn any into optionals once. Internals stay sealed and precise.
Put this Script in ServerScriptService.
luau
--!strict
export type Crew = { name: string, size: number }
local function makeCrew(name: string): Crew
return { name = name, size = 4 }
end
local got = makeCrew("Fox")This block checks clean. You infer got as Crew. Construction finishes inside the maker. No later growth is needed. Callers read a sealed, complete value.
Minimal repros shrink the search
A small failing file beats a large red project. You cut until only the break remains.
You copy the failing function and its types into a fresh Script. You delete unrelated fields, branches, and calls. You stop when the file is tiny but still red. You fix there, then you carry the fix back.
Some failures need a character, a remote, or a DataStore shape. You stub those inputs with literals of the same type. You match on slot types and locations when Studio wording differs from the prose here.
Rechecks take seconds. Diffs stay small. Reviewers see the root, not the project.
Put this Script in ServerScriptService.
luau
--!strict
export type Pet = { name: string }
local pet: Pet = { name = "Pip" }
local name = pet.nameThis block checks clean. It is your repro baseline. You regrow toward the failing shape one field at a time. The line that turns red names the culprit.
Common mistakes
Wrong: you fix the last error first.
Put this Script in ServerScriptService.
luau
--!strict
export type Box = { coins: number }
local box: Box = { coins = "many" }
local got = box.coins + 1This block errors at the literal first. The addition error is a cascade. You fix the literal, then you recheck. Editing only the addition leaves the root red.
luau
--!strict
export type Box = { coins: number }
local box: Box = { coins = 9 }
local got = box.coins + 1This fix checks clean. You infer got as number.
Wrong: you quote a remembered message instead of reading the slot.
Put this Script in ServerScriptService.
luau
--!strict
local total = 10 + "5"This block errors as a string-into-number mismatch. You fix the types, not the remembered prose. Studio wording can differ by release, so slot types do not lie.
luau
--!strict
local total = 10 + 5This fix checks clean. You infer total as number.
Wrong: you keep an any to keep red away.
Put this Script in ServerScriptService.
luau
--!strict
local state: any = { coins = 2 }
local next = state.coins + 1This block may pass while next becomes any. Later errors stay hidden. You fix it with a record.
luau
--!strict
export type State = { coins: number }
local state: State = { coins = 2 }
local next = state.coins + 1This fix checks clean. You infer next as number.
Exercises
- Classify a string-into-number call and fix it.
- Fix a missing-required-prop literal two ways.
- Repair a two-return function with one short path.
- Break a two-hop nilable chain with early returns.
- Shrink a failing file to a ten-line repro.
Solution 1
Put this Script in ServerScriptService.
luau
--!strict
local function pay(coins: number): number
return coins * 2
end
local got = pay(6)This block checks clean. You infer got as number. The wrong call pay("6") errors as a conversion mismatch. Passing a number cures the slot.
Solution 2
Put this Script in ServerScriptService.
luau
--!strict
export type Badge = { id: string, score: number? }
local a: Badge = { id = "fox" }
local b: Badge = { id = "fox", score = 9 }This block checks clean. You infer both as Badge. Adding the value or marking the field optional both cure missing-prop errors. You pick optional only when absence is valid.
Solution 3
Put this Script in ServerScriptService.
luau
--!strict
local function split(full: string): (string, string)
local at = string.find(full, " ")
if at == nil then
return full, ""
end
return string.sub(full, 1, at - 1), string.sub(full, at + 1)
end
local x, y = split("A B")This block checks clean. You infer x as string and y as string. Both paths send two strings. The short path was the arity bug.
Solution 4
Put this Script in ServerScriptService.
luau
--!strict
local function zoneName(model: Model?): string
if model == nil then
return "none"
end
local zone = model:FindFirstChild("Zone")
if zone == nil then
return "none"
end
return zone.Name
end
local got = zoneName(nil)This block checks clean. You infer got as string. Each hop gets its own guard and early return. No chained call crosses a nilable.
Solution 5
Put this Script in ServerScriptService.
luau
--!strict
export type Coin = { amount: number }
local coin: Coin = { amount = 2 }
local got = coin.amount + 1This block checks clean. You infer got as number. You grow from this baseline toward the real failure until red returns. That line is your fix site.
Summary
- You read expected versus got at the first error.
- Eight shapes cover most strict red: conversion, missing prop, optional return, arity, sealed growth, any leak, bad cast, and widened generic.
- Arity must match on every path.
- Nilable chains need one guard per hop.
- You build before sealing and parse any at the edge.
- You shrink to a minimal repro, fix there, and carry the fix back.
- Next: Architecture.