Troubleshooting¶
This page covers the most common mistakes when using quent, with exact error messages, explanations, and fixes.
1. Forgetting return Before Q.return_()¶
Symptom¶
Your linter reports unreachable code after Q.return_(), or readers are confused about the control flow:
def process(x):
if x < 0:
Q.return_('negative') # <-- missing `return`
print('this looks unreachable') # linter warning
return x * 2
What happens¶
Q.return_() works by raising an internal _Return exception. Because it raises immediately, the call itself will exit the function -- so the pipeline does receive the early-return signal even without return. However, omitting return is misleading: it looks like Q.return_() is a no-op side-effect call, and linters will flag subsequent code as unreachable.
Fix¶
Always write return Q.return_(...):
Tip
Using return makes the control flow explicit -- both for human readers and for static analysis tools. The value returned by Q.return_() is never actually used (it raises before returning), but the return statement communicates intent.
2. Using Q.break_() Outside Iteration¶
Error¶
quent.QuentException: Q.break_() cannot be used outside of a loop or iteration context (foreach, foreach_do, iterate, iterate_do, flat_iterate, flat_iterate_do, while_).
Cause¶
Q.break_() propagates outward through Q boundaries, if_() predicates and branches, with_/with_do bodies, drive_gen, and other non-iteration positions, looking for the nearest iteration scope (foreach/foreach_do/iterate/iterate_do/flat_iterate/flat_iterate_do/while_). If none catches it, it surfaces as QuentException at the outermost run().
from quent import Q
# WRONG -- break_() in a .then() step (no iteration scope)
result = (
Q(5)
.then(lambda x: Q.break_(x) if x > 3 else x)
.run()
)
# raises QuentException
Fix¶
Pick the signal that matches your intent:
| You want | Use |
|---|---|
Exit the current Q early (Python-return-style) |
Q.return_(v) |
| Stop the nearest iteration / loop | Q.break_(v) -- but only from inside one |
| Exit the entire top-level pipeline from any depth | Q.exit_(v) |
from quent import Q
# Use return_() to exit the current Q early
result = Q(5).then(lambda x: Q.return_(x) if x > 3 else x * 2).run()
# result == 5
# Use exit_() to exit the entire pipeline regardless of nesting
result = Q(5).then(lambda x: Q.exit_(x) if x > 3 else x * 2).run()
# result == 5
# Use break_() inside an iteration scope
result = Q([1, 2, 3, 4, 5]).foreach(lambda x: Q.break_() if x > 3 else x * 2).run()
# result == [2, 4, 6]
Warning
Q.break_() is also invalid inside .except_()/.finally_() handlers and inside .gather() workers. Q.return_() is also invalid in handlers (but allowed in gather()/drive_gen — see §13 below). Q.exit_() is always allowed.
Changed in 7.0.0
The wording of this error changed; Q.break_() no longer raises a specific message from inside if_() predicates, with_ bodies, drive_gen, or nested-Q steps -- it now propagates through them looking for an iteration scope.
3. Calling .else_() or .else_do() Without Preceding .if_()¶
Error¶
If the pipeline has no steps at all:
quent.QuentException: else_() requires a preceding if_() — the pipeline has no steps yet.
Usage: q.if_(predicate).then(handler).else_(alternative)
If the last step is not an .if_() or .then()/.do() following .if_():
quent.QuentException: else_() must follow immediately after if_() with no operations
in between. The last operation in this pipeline is not if_().
Usage: q.if_(predicate).then(handler).else_(alternative)
Cause¶
.else_() and .else_do() must be chained immediately after .if_().then() or .if_().do(). Any intervening operation breaks the association:
from quent import Q
# WRONG -- .do(print) between .if_().then() and .else_()
result = (
Q(value)
.if_(lambda x: x > 0).then(process_positive)
.do(print)
.else_(process_negative) # raises QuentException
.run()
)
Fix¶
Ensure .else_() immediately follows .if_():
from quent import Q
# Correct
result = (
Q(value)
.if_(lambda x: x > 0).then(process_positive)
.else_(process_negative)
.do(print) # side-effects go after the if/else block
.run()
)
Tip
Think of .if_() and .else_() as a single unit. Build the complete conditional block first, then add subsequent pipeline steps.
4. Passing a Non-Callable to .do()¶
Error¶
Cause¶
.do() enforces that its argument is callable. A non-callable .do(42) would silently do nothing (the value is discarded since .do() is a side-effect step), so this is caught at build time.
Fix¶
Use .then() for literal values, .do() only for callables:
from quent import Q
# .then() for literal values
result = Q(42).then('replacement').run() # result == 'replacement'
# .do() for side-effects
result = Q(42).do(print).run() # prints 42, result == 42
.then() vs .do() -- key difference
.then(v)accepts any value. Non-callables replace the current pipeline value..do(fn)requires a callable. Its return value is always discarded.
5. Expecting except_() to Catch KeyboardInterrupt¶
Symptom¶
You press Ctrl+C during pipeline execution and expect your handler to catch it, but it propagates instead.
Cause¶
.except_() catches Exception by default, not BaseException. KeyboardInterrupt and SystemExit are BaseException subclasses that are not Exception subclasses.
Fix¶
If you genuinely need to catch BaseException subclasses, specify them explicitly:
from quent import Q
result = (
Q(long_running_task)
.except_(handle_error, exceptions=(Exception, KeyboardInterrupt))
.run()
)
Think twice
Catching KeyboardInterrupt or SystemExit prevents users from terminating your program. quent emits a warning when you do this. In almost all cases, use .finally_() for cleanup instead -- finally handlers run even when these signals propagate.
from quent import Q
# Preferred: finally_() for cleanup, let interrupts propagate
result = (
Q(long_running_task)
.except_(handle_recoverable_error)
.finally_(lambda _: cleanup())
.run()
)
6. Duplicate Handler Registration¶
Error¶
Cause¶
Each pipeline supports at most one except_() and one finally_(). This is enforced at registration time.
Fix¶
Consolidate into a single handler, or use nested pipelines for per-section error handling:
from quent import Q
# Consolidate
def combined_handler(exc):
if isinstance(exc, ConnectionError):
return handle_connection_error(exc)
if isinstance(exc, ValueError):
return handle_validation_error(exc)
raise exc
q = Q(data).then(process).except_(combined_handler)
# Or use nested pipelines
fetch_q = Q().then(fetch_data).except_(handle_fetch_error)
process_q = Q().then(validate).then(transform).except_(handle_process_error)
pipeline = (
Q(url)
.then(fetch_q)
.then(process_q)
.finally_(lambda _: cleanup())
.run()
)
7. TypeError When Copying a Q Instance¶
Error¶
Cause¶
copy.copy() and copy.deepcopy() are blocked on Q instances. A shallow copy would produce a broken object with shared linked-list node references, leading to subtle corruption. A deep copy is semantically undefined for objects containing arbitrary callables.
Fix¶
Use .clone() to produce a correct independent copy:
from quent import Q
base = Q().then(validate).then(transform)
branch_a = base.clone().then(to_json) # independent copy
branch_b = base.clone().then(to_record) # independent copy
8. Concurrency Parameter Validation Errors¶
Errors¶
Cause¶
The concurrency parameter has strict validation:
- Must be a positive integer,
-1(unbounded), orNone. Booleans are rejected. - Must be
>= 1or-1.
Fix¶
# WRONG
Q(urls).foreach(fetch, concurrency=True) # bool rejected
Q(urls).foreach(fetch, concurrency=0) # must be >= 1
# RIGHT
Q(urls).foreach(fetch, concurrency=4)
Q(urls).foreach(fetch, concurrency=-1) # unbounded
9. Pipeline Nesting Depth Exceeded (Visualization Limit)¶
Error¶
Cause¶
Nested pipeline visualization is truncated at depth 50. This is a rendering limit only — there is no execution depth limit. Deeply nested pipelines execute without restriction; only the traceback/repr visualization is truncated.
Fix¶
If the truncated visualization is unhelpful, flatten your pipeline instead of nesting deeply:
from quent import Q
# Instead of deep nesting, compose steps in a single pipeline
q = Q(42)
for step in processing_steps:
q = q.then(step)
result = q.run()
10. Enhanced Traceback Opt-Out¶
Symptom¶
quent's traceback modifications interfere with your debugger, CI system, or custom exception handler.
Fix: Environment Variable¶
Set before importing quent:
This disables all traceback modifications: visualization injection, frame cleaning, and hook patching.
Suppressing Values in Tracebacks¶
To keep pipeline visualizations but hide sensitive data:
This replaces argument values with type-name placeholders (e.g., <str> instead of 'secret_api_key').
11. ExceptionGroup from Concurrent Operations¶
Symptom¶
Multiple concurrent workers (in gather(), or foreach()/foreach_do() with concurrency) fail simultaneously, producing an ExceptionGroup instead of a single exception.
Cause¶
When multiple concurrent workers fail, all exceptions are wrapped in an ExceptionGroup. A single failure propagates directly (no wrapping).
Fix¶
Handle ExceptionGroup using except* (Python 3.11+) or the .subgroup() / .split() methods:
from quent import Q
try:
result = (
Q(urls)
.foreach(fetch, concurrency=4)
.run()
)
except* ConnectionError as eg:
print(f'{len(eg.exceptions)} connection errors')
except* ValueError as eg:
print(f'{len(eg.exceptions)} value errors')
Or catch ExceptionGroup in the pipeline's except_()
result = (
Q(urls)
.foreach(fetch, concurrency=4)
.except_(lambda ei: handle_group(ei.exc) if isinstance(ei.exc, ExceptionGroup) else handle_single(ei.exc))
.run()
)
12. Async Transition from Sync Pipeline Handlers¶
Symptom¶
Your sync pipeline's run() returns a coroutine instead of a plain value.
Cause¶
A sync pipeline's finally_() or except_() handler returned a coroutine. The engine performs an async transition: run() returns a coroutine instead of a plain value.
Fix¶
Either:
- Use
await q.run()in an async context so the handler is properly awaited. - Make the handler synchronous.
# Option 1: await in async context
result = await q.run()
# Option 2: sync handler
q.finally_(lambda rv: sync_cleanup(rv))
13. Control Flow Signals in Handlers¶
Error¶
Cause¶
Q.return_() and Q.break_() are not allowed when raised directly inside except_() or finally_() handlers -- they would skip handler invariants and the "always runs" guarantee. Q.exit_() is allowed in both (it propagates unconditionally).
Fix¶
Return the value directly, or use Q.exit_() if your intent is to terminate the entire pipeline from inside the handler:
from quent import Q
# WRONG -- return_() in except handler
q = Q(data).then(process).except_(
lambda exc: Q.return_('fallback') # raises QuentException
)
# RIGHT -- use the handler's return value
q = Q(data).then(process).except_(
lambda exc: 'fallback' # handler's return value replaces the result
)
# RIGHT -- exit_() bypasses the trap when you actually want to abort
q = Q(data).then(process).except_(
lambda exc: Q.exit_('aborted') # value surfaces at outermost run()
)
Nested-Q handler: Q.return_() is absorbed locally¶
When the handler is registered on a nested Q, that nested Q absorbs its own Q.return_() -- the handler "returns" with the signal's value and the outer pipeline continues:
inner = Q().then(may_fail).except_(lambda ei: Q.return_('fallback'))
outer = Q(data).then(inner).then(next_step)
outer.run()
# inner's handler absorbs the signal; 'fallback' flows to next_step as inner's result.
# (Pre-7.0 this raised QuentException; the handler trap now applies only to direct invocation.)
14. Q.return_() Doesn't Exit the Entire Pipeline From a Nested Q¶
Symptom¶
You have a nested Q (outer.then(inner)) and call Q.return_() from inside inner. You expected the whole pipeline to short-circuit but the outer pipeline keeps running with the returned value as inner's result.
Cause¶
Since 7.0.0, Q.return_() returns from the current Q only -- Python-return-style. Each Q boundary absorbs its own signal. Pre-7.0, Q.return_() propagated to the outermost run().
Fix¶
Use Q.exit_() for the old "exit the entire pipeline from arbitrary depth" semantics:
inner = Q().then(lambda x: Q.exit_('STOP') if x < 0 else x).then(lambda x: x * 2)
outer = Q(-5).then(inner).then(lambda x: x + 100)
outer.run()
# 'STOP' -- exit_() bypasses inner's boundary AND outer's; absorbed at outermost run().
Same applies if you used Q.return_() inside a gather() worker or drive_gen fn expecting the whole pipeline to abort -- those positions now return from the worker / fn (per §7.4 carve-out). Switch to Q.exit_().