---
url: /strict-typing/diagnosing-errors.md
description: Read expected-versus-got errors and fix the nine most common type failures.
---

# Diagnosing Errors

*Written: {{ $frontmatter.date }}*

## 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

* [Solver and Migration](./solver-and-migration.md)
* [Subtyping and Compatibility](./subtyping-and-compatibility.md)
* [Type Packs and Variadics](./type-packs-and-variadics.md)

## 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](https://create.roblox.com/docs/en-us/luau/type-checking). The [luau.org types](https://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](./annotations-and-inference.md), [Narrowing and Guards](./narrowing-and-guards.md), [Casts and Assertions](./casts-and-assertions.md), and [Solver and Migration](./solver-and-migration.md).

The limitation is masking. One root can paint many lines. Suppression trims cascades per [type error suppression](https://rfcs.luau.org/type-error-suppression.html). 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 = 4
```

This 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
end
```

This 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"
end
```

This 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
end
```

This 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"
end
```

This 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 = 1
```

This 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 = 1
```

This fix checks clean. You infer `pet` as `{ name: string, hunger: number }`. The literal starts unsealed per [unsealed literals](https://rfcs.luau.org/unsealed-table-literals.html).

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 + 4
```

This 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 :: string
```

This wrong pattern errors. The pair is unrelated, so bidirectional casting rejects it per [syntax type ascription bidi](https://rfcs.luau.org/syntax-type-ascription-bidi.html). 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](https://create.roblox.com/docs/en-us/luau/type-checking) and the [syntax tour](https://luau.org/syntax/).

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)
end
```

This 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](./narrowing-and-guards.md).

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](https://luau.org/types/tables/) and [sealed subtyping](https://rfcs.luau.org/sealed-table-subtyping.html). You replace `any` with records plus guards at package edges per [Casts and Assertions](./casts-and-assertions.md).

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.name
```

This 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 + 1
```

This 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 + 1
```

This 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 + 5
```

This 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 + 1
```

This 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 + 1
```

This fix checks clean. You infer `next` as `number`.

## Exercises

1. Classify a string-into-number call and fix it.
2. Fix a missing-required-prop literal two ways.
3. Repair a two-return function with one short path.
4. Break a two-hop nilable chain with early returns.
5. Shrink a failing file to a ten-line repro.

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

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

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

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

::: details Solution 5
Put this Script in ServerScriptService.

```luau
--!strict
export type Coin = { amount: number }
local coin: Coin = { amount = 2 }
local got = coin.amount + 1
```

This 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](../advanced/architecture.md).
