RSS

wheels console: The Debugging Move You're Not Using

There’s a debugging move every Rails developer has muscle-memorized: something’s weird, so you open rails console, poke the actual application — real models, real config, real database — and know in thirty seconds what would have taken thirty minutes of writeDump()-and-refresh archaeology.

Wheels 4 has that move: wheels console. It connects to your running dev server and evaluates whatever you type inside the live application context — models resolve, scopes chain, the DI container answers, settings read back. This post is the working tour: what’s available at the prompt, the slash-commands, the sessions that replace dump-and-refresh, and the difference between console and its lookalike, repl.

What the prompt can see

Start a dev server, then from the project root:

wheels console

You get a wheels> prompt wired into the app. Anything that would compile inside a CFML component is fair game, and the surfaces you’ll actually reach for:

SurfaceExample
Model findersmodel("User").findAll()
Model instancesmodel("User").new(email="test@example.com")
Scopes & chainsmodel("Post").published().where("authorId", 1).get()
DI servicesservice("emailService")
Framework settingsget("environment"), get("dataSourceName")
Live app globalsapplication.wheels.version, application.wheels.routes
Plain CFMLnow(), arrayLen([1,2,3])

Results come back formatted for a terminal: queries as column-aligned tables, model instances as key-value blocks, structs and arrays pretty-printed as JSON, primitives inline after =>.

The sessions that pay for themselves

“Why is this record invalid?” — the classic. Instead of a form, a submit, and a dump:

wheels> var u = model("User").new(email="bad")
wheels> u.valid()
=> false

wheels> u.errorsOn("email")
=> [
    {
      "message": "must be a valid email address"
    }
  ]

Three lines, and you’re reading the actual validation output of the actual model class the running app loaded — not your assumption about it.

“Is my scope chain building the query I think it is?”

wheels> model("Post").published().recent().findAll().recordCount
=> 17

Seventeen, not the forty you expected? Now you know the problem is in the scope definitions, not the view — before you’ve touched a template.

“Did my service register correctly?” — the DI question that otherwise surfaces as a 500 three requests later:

wheels> var svc = service("emailService")
wheels> svc.isConfigured()
=> true

“What does the app think its config is?” — not what settings.cfm says; what the merged, per-environment, .env-resolved runtime actually holds. get("environment"), get("dataSourceName"), one keystroke each. If you read our configuration post, this is the fastest way to confirm which of the two systems won.

The slash-commands

Lines starting with / are shortcuts for inspections you’d otherwise type long-form:

CommandDoes
/modelsEvery registered model, alphabetized
/routesAll routes as pattern -> controller#action
/envThe current environment struct
/dsThe current datasource name
/versionThe Wheels version the app is actually running
/reloadReload the application — same as ?reload=true
/help, /clear, /exitWhat they say

/routes deserves a highlight: “which route does this URL actually hit?” is a question the console answers in one line, with the same data the dispatcher uses. And /reload closes the loop on a config-editing session — change settings.cfm in your editor, /reload in the console, get() the value to confirm, never touching a browser.

console is not repl

The CLI ships two prompts that look alike and aren’t:

wheels consolewheels repl
Promptwheels>cfml>
App contextFull Wheels appNone — bare CFML evaluator
Models / DI resolve?YesNo
Requires a running server?YesNo

repl is a calculator: quick CFML expression evaluation anywhere, no project needed. console is a stethoscope: it needs a running server precisely because its whole value is evaluating inside that server’s state. If you type model("User") into repl and get an error, nothing’s broken — you’re in the wrong tool.

Two habits worth forming

Verify state after mutations, not from memory. After a migration, model("Post").new() in the console and eyeball the properties — the schema the ORM read, not the schema you meant. After a seed run, model("Role").findAll() — did seedOnce() match on the unique property or create a duplicate? The console reads the same application state the next request will use, so what you see is what production logic gets.

Treat it as executable, so treat it with respect. model("User").deleteAll() at the console prompt does exactly what it does anywhere else — this is a live evaluator against your dev database, not a sandbox. That immediacy is the point; it just cuts both ways.

One boundary worth stating plainly: the console is a development-workflow tool wired to your dev server. For inspecting production state, the answer is the observability plumbing — request IDs, health endpoints, and structured logs — not a REPL against a production box.

If you don’t use the wheels CLI, this is one of the few features on the honest list with no in-app equivalent — the interactivity is the feature. It’s also, in our experience, the single best argument for having the CLI installed alongside whatever else you use: the full reference is at Console & REPL in the CLI guides.

Comments

Newsletter

Release notes and new posts, once a month. No spam.

Prefer RSS? Subscribe to the feed →