# ERROR 42601: Syntax Error in Postgres

Source: https://www.bytebase.com/reference/postgres/error/42601-syntax-error-postgres/

---

## Error Message

```sql
ERROR: syntax error at or near "FROM"
LINE 1: SELECT * FORM users
                ^
SQLSTATE: 42601
```

The error always points at the **first token PostgreSQL could not parse**, with a caret (`^`) under the offending position. The actual mistake is often one or two tokens earlier — Postgres only notices the problem when it hits the next thing it can't make sense of.

## What Triggers This Error

42601 is a parse-time error. It fires before the planner ever sees your query, so it never depends on table contents or permissions. The most common triggers:

- **Misspelled keyword** — `FORM` instead of `FROM`, `SLECT` instead of `SELECT`, `WERE` instead of `WHERE`
- **Reserved word used as identifier without quotes** — column named `user`, `order`, `group`, `table`, `select`
- **Missing or misplaced punctuation** — trailing comma before `FROM`, missing comma between columns, unclosed parenthesis
- **Mixed quote styles** — using `"` (identifier quotes) where `'` (string literals) is required
- **Postgres-specific syntax in MySQL/SQL Server style** — `LIMIT 10, 20` (MySQL pagination), `TOP 10` (SQL Server), backticks for identifiers
- **Version-mismatch features** — `MERGE` on PostgreSQL 14 or earlier, `NULLS NOT DISTINCT` on PostgreSQL 14 or earlier
- **Dollar-quoted strings with unmatched tags** — `$func$ ... $func2$` inside a function body
- **Stray semicolon inside a `CASE`, function, or `DO` block** — closing the statement early

## Fix by Scenario

### Misspelled keyword

The error caret usually points at the next token, not the typo itself. Read the error in reverse: the keyword *before* the caret position is where the error usually is.

```sql
SELECT * FORM users WHERE id = 1;
--       ^^^^ caret will be at "users" because Postgres expected `FROM` and got `FORM`
```

Fix: correct the keyword. If your SQL is generated, log the final query string before sending — most "phantom" 42601 errors come from string concatenation that drops a space or adds an extra one.

### Reserved word as identifier

PostgreSQL has a [list of reserved keywords](https://www.postgresql.org/docs/current/sql-keywords-appendix.html). If you use one as a column or table name without double-quoting it, you get 42601.

```sql
-- fails
CREATE TABLE order (id BIGINT, total NUMERIC);
-- ERROR: syntax error at or near "order"

-- works (but you'll have to quote it forever after)
CREATE TABLE "order" (id BIGINT, total NUMERIC);

-- best: rename the table
CREATE TABLE orders (id BIGINT, total NUMERIC);
```

Common offenders: `user`, `order`, `group`, `table`, `select`, `from`, `where`, `case`, `when`, `then`, `end`, `array`, `cast`, `current_user`, `session_user`. Renaming the column is almost always cheaper than living with the quoting forever.

### Trailing comma or missing comma

```sql
-- trailing comma before FROM
SELECT id, name, email, FROM users;
-- ERROR: syntax error at or near "FROM"

-- missing comma between columns
SELECT id name email FROM users;
-- ERROR: syntax error at or near "name"
```

These are mostly editor-paste artifacts. A SQL formatter (e.g. `pg_format`, `sqlfluff`) catches both before they reach the database.

### Mixed quote styles

PostgreSQL uses single quotes for string literals and double quotes for identifiers. They are not interchangeable — and they have opposite meanings from MySQL where backticks quote identifiers and either kind of quote works for strings.

```sql
-- fails: "alice" is interpreted as a column name
SELECT * FROM users WHERE name = "alice";
-- ERROR: column "alice" does not exist (sometimes 42601, sometimes 42703)

-- works
SELECT * FROM users WHERE name = 'alice';
```

If you migrated SQL from MySQL, search for backticks (`` ` ``) and double-quoted string-like values — both are 42601 magnets.

### MySQL or SQL Server syntax used in Postgres

PostgreSQL does not understand:

| MySQL / SQL Server | PostgreSQL equivalent |
|--------------------|----------------------|
| `LIMIT 10, 20` (offset, count) | `LIMIT 20 OFFSET 10` |
| `SELECT TOP 10 ...` | `SELECT ... LIMIT 10` |
| `` SELECT `id` FROM `users` `` | `SELECT id FROM users` (no backticks) |
| `IFNULL(x, y)` | `COALESCE(x, y)` |
| `DATETIME` column type | `TIMESTAMP` |
| `AUTO_INCREMENT` | `GENERATED ALWAYS AS IDENTITY` or `SERIAL` |

If a query was copy-pasted from a different dialect, the 42601 caret will point at the first incompatible token.

### Version-mismatch features

Some SQL features are only valid in newer PostgreSQL versions:

```sql
-- ERROR on PostgreSQL 14 or earlier — MERGE is PostgreSQL 15+
MERGE INTO target USING source ON ...

-- ERROR on PostgreSQL 14 or earlier — NULLS NOT DISTINCT is 15+
CREATE UNIQUE INDEX ON t (col) NULLS NOT DISTINCT;
```

Check your server version before assuming a syntax error is a typo:

```sql
SELECT version();
SHOW server_version_num;  -- 150000 means 15.0, 140000 means 14.0
```

If you must support an older version, rewrite the statement (`INSERT ... ON CONFLICT` instead of `MERGE`, partial unique index instead of `NULLS NOT DISTINCT`).

### Dollar-quoted strings with mismatched tags

PostgreSQL allows custom tags inside dollar quotes to safely embed code that contains `$$`:

```sql
-- works: matching tags
CREATE FUNCTION greet() RETURNS text AS $body$
BEGIN
    RETURN 'hello';
END;
$body$ LANGUAGE plpgsql;

-- fails: mismatched tags
CREATE FUNCTION greet() RETURNS text AS $body$
BEGIN
    RETURN 'hello';
END;
$func$ LANGUAGE plpgsql;
-- ERROR: syntax error at or near "$func$"
```

When generating function bodies programmatically, pick a unique tag string and use it consistently for the open and close.

### Stray semicolon inside a function or DO block

A `DO` block, anonymous block, or function body is delimited by dollar quotes — semicolons inside don't end the outer statement, but if you forget the closing dollar quote, the parser sees the semicolon as the end and fails on the next token.

```sql
-- fails: missing closing $$
DO $$
BEGIN
    RAISE NOTICE 'hello';
END
LANGUAGE plpgsql;
-- ERROR: syntax error at or near "LANGUAGE"
```

Add the closing dollar quote and the language clause becomes a separate (correct) statement:

```sql
DO $$
BEGIN
    RAISE NOTICE 'hello';
END
$$ LANGUAGE plpgsql;
```

### Generated SQL with empty bind values

ORMs and query builders sometimes emit invalid SQL when a list parameter is empty:

```sql
-- generated when ids = []
SELECT * FROM users WHERE id IN ();
-- ERROR: syntax error at or near ")"
```

Fix at the application layer — short-circuit the query when the list is empty, or use a sentinel:

```sql
SELECT * FROM users WHERE id IN (NULL);  -- always returns empty, but parses
```

Better: the application should not issue a query at all when the input list is empty.

## Prevention

- Run SQL through a formatter or linter (`pg_format`, `sqlfluff`) in CI — most 42601 errors are caught at the formatter stage
- Quote any identifier that would otherwise be a reserved word, or rename the column to something safe
- When porting SQL from another database, search for `` ` ``, `LIMIT n, m`, `TOP`, and `IFNULL` patterns first
- For dynamic SQL, log the final query string before execution — guessing at the failing query from production logs is the slowest way to debug 42601
- When using new syntax (`MERGE`, `NULLS NOT DISTINCT`, etc.), check the minimum supported PostgreSQL version in your fleet, not just your dev machine
- Configure ORMs and query builders to short-circuit empty `IN` lists rather than emitting `IN ()`

> **Note:** [Bytebase's SQL Review](https://docs.bytebase.com/sql-review/review-rules/) parses every change against your target PostgreSQL version before it runs, catching version-mismatch syntax (e.g. `MERGE` on PG 14) and reserved-word collisions during change review rather than at deploy time. See also [ERROR 42703: Undefined Column](/reference/postgres/error/42703-undefined-column-postgres) for the closely-related "column does not exist" failure.