drizzle-team / drizzle-team/drizzle-orm

[BUG]: drizzle-kit migrate exits 1 with no visible error message — progress spinner overwrites the real error

Open
#6,121 4 comments 1 reaction 0 assignees View on GitHub
Dominant language
TypeScript
Stars
35.8k
Forks
1.6k
Avg merge
2d 7h
Merged PRs (30d)
4

Description

### Verifications (template's checkbox group)

- [x] I have verified that the bug I'm about to report hasn't been filed before.

### What version of `drizzle-orm` are you using?

```
0.45.2
```

### What version of `drizzle-kit` are you using?

```
0.31.10
```

### Other packages

```
postgres@3.4.9 (via @drizzle-orm/postgres-js)
hanji15 (internal, used for the CLI spinner)
```

### Describe the Bug

**Undesired behavior**

`drizzle-kit migrate` exits with code `1` when a migration fails, **but the
error message is not visible in any non-TTY capture** (CI logs, `docker logs`,
redirected stdout, log aggregators). The CLI's progress spinner uses ANSI
escape sequences to **erase the previous line and rewrite it**, so the real
error message ends up on a line that the next spinner redraw replaces.

The evidence captured by `docker logs` looks like this:

```
[⣷] applying migrations...{
severity_local: 'NOTICE',
severity: 'NOTICE',
code: '42P07',
message: 'relation "app_user_account" already exists, skipping',
file: 'parse_utilcmd.c',
line: '208',
routine: 'transformCreateStmt'
}
[2K[1G[⣯] applying migrations...
```

No `ERROR` line. No stack trace. Container exits 1, but the operator has
no way to know **why**.

**Steps to reproduce**

1. Set up a postgres instance with a `public` schema that already has
tables (or any state that will conflict with your first migration).
2. Configure `drizzle.config.ts` with a custom `migrations.table` /
`migrations.schema` (this is a common pattern to isolate the migrations
journal from `drizzle.__drizzle_migrations`).
3. Run `drizzle-kit migrate` inside a Docker container (or any context
that strips the TTY — CI runners, redirected stdout, journald captures).
4. Observe that the container exits 1 but the error message is not
readable in `docker logs`.

**Real-world triggering examples** (observed in our environment):

1. `relation "app_user_account" already exists` — Postgres 42P07. Thrown by
`drizzle-orm/pg-core/dialect.cjs` line ~62 in `PgDialect.migrate`,
caught by the migrator wrapper, then re-thrown after the spinner
overwrites the line.
2. `type "audit_event_kind" does not exist` — Postgres
42704. Same class: the migrator wraps the original error but
the CLI spinner hides it.

**Desired result**

The CLI should either:
- **Print the error to stderr** (not stdout, so it doesn't get
overwritten by the spinner), OR
- **Honor a `--no-progress` / `--no-pretty` flag** that bypasses the
spinner entirely and prints plain text per migration, OR
- **Make the spinner write to stderr** instead of stdout so
captured logs preserve at least the spinner frame.

The minimum viable fix is one of those three. The first option is the
smallest change and the highest-value for non-TTY capture.

**Are you working in a monorepo?**

Yes — monorepo.

**If this is a bug related to types: What Typescript version are you using?**

n/a — runtime bug, not a typescript issue.

**If you're using a runtime that isn't Node.js: Which one?**

n/a — Node.js 24.x.

---

### Suggested reproduction (minimal)

```bash
# 1. Start a postgres container with a pre-existing 'public.account' table
docker run -d --name pg-test -p 5533:5432 \
-e POSTGRES_USER=u -e POSTGRES_PASSWORD=p -e POSTGRES_DB=postgres postgres:16
docker exec pg-test psql -U u -d postgres -c \
"CREATE TABLE public.account (id text PRIMARY KEY);"

# 2. Run drizzle-kit migrate from a fresh image (no TTY)
docker run --rm -i --network host \
-e POSTGRES_URL_APP=postgresql://u:p@host.docker.internal:5533/postgres \
your-migrate-image:latest \
drizzle-kit migrate --config=./drizzle.config.ts > /tmp/out.log 2> /tmp/err.log
echo "exit=$?"
cat /tmp/err.log # <-- empty
cat /tmp/out.log # <-- spinners + NOTICEs, no ERROR
```

`exit=1`, `err.log` is empty, `out.log` has no error message. The bug.

---

### Proposed fix (one-line concept)

In `bin.cjs` near line 92050 (the `migrate` command handler), the call:

```js
await renderWithTask(new MigrateProgress(), migrate2({...}));
```

becomes:

```js
try {
await renderWithTask(new MigrateProgress(), migrate2({...}));
} catch (err) {
process.stderr.write(`[migrate] FAILED: ${err.message}\n`);
if (err.cause?.message) {
process.stderr.write(`[migrate] CAUSE: ${err.cause.message}\n`);
}
process.exit(1);
}
```

This guarantees the error is written to **stderr** (which the spinner
doesn't touch) and gives operators a reliable signal in CI logs.

Optionally, add a `--no-progress` flag that bypasses `renderWithTask`
and prints plain text — useful for CI pipelines that prefer
deterministic line counts.

---

### Environment

- drizzle-kit 0.31.10
- drizzle-orm 0.45.2
- postgres 16 (also reproducible on postgres 18)
- Node.js 24.19.0
- Docker Desktop 4.x on Windows (where the spinner escape codes are
captured but the line-rendering is lost)

---

### Workaround (for users who hit this)

The three-step bypass:

1. **Direct drizzle-orm migrator call** via `node -e` to bypass the
CLI spinner entirely:
```js
const { drizzle } = require('drizzle-orm/postgres-js');
const { migrate } = require('drizzle-orm/postgres-js/migrator');
const db = drizzle(sql);
await migrate(db, { migrationsFolder: '/app/drizzle/migrations' });
```
2. **Apply migrations manually via psql** with `-X -v ON_ERROR_STOP=1`.
3. **Volume-mount + redirect** to files inside the container
(this still suffers from the spinner overwrite but preserves the
notice log).

The first workaround (direct migrator API) is the only reliable way
to see the error without the upstream fix.

---

Contributor guide

Open the contributing guide

Research direction

Start in bin.cjs near line 92050 at the migrate command handler, and inspect renderWithTask, MigrateProgress, and the migrate2 call. Run the suggested Docker reproduction with stdout and stderr redirected to confirm the failure is hidden. Done means a non-TTY migration failure preserves a readable error in captured logs while retaining the exit code 1.

Written by the indexing model from the issue text.

Assessment

Tech stack
docker, node.js, postgres, typescript
Domain
cli, databases
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.