Commit 95b844d

Anton Golub <antongolub@antongolub.com>
2025-07-28 15:00:26
fix: handle `nothrow` on `ProcessPromise` init stage (#1288)
* fix: handle `nothrow` on `ProcessPromise` init stage * chore: update lockfile and tests * chore: enhance `ProcessOutput.fromError` type * chore: make private `ProcessPromise.build` * chore: bytes shrinking
1 parent 5e4b543
build/core.cjs
@@ -396,6 +396,7 @@ var import_util2 = require("./util.cjs");
 var CWD = Symbol("processCwd");
 var SYNC = Symbol("syncExec");
 var EPF = Symbol("end-piped-from");
+var SHOT = Symbol("snapshot");
 var EOL = import_node_buffer.Buffer.from(import_node_os.EOL);
 var BR_CC = "\n".charCodeAt(0);
 var DLMTR = /\r?\n/;
@@ -434,24 +435,20 @@ var defaults = resolveDefaults({
   timeoutSignal: SIGTERM
 });
 var storage = new import_node_async_hooks.AsyncLocalStorage();
-var box = ((box2 = []) => ({
-  push(item) {
-    if (box2.length > 0) throw new Fail(`Box is busy`);
-    box2.push(item);
-  },
-  loot: box2.pop.bind(box2)
-}))();
 var getStore = () => storage.getStore() || defaults;
-var getSnapshot = (opts, from, cmd) => __spreadProps(__spreadValues({}, opts), {
+var getSnapshot = (opts, from, pieces, args) => __spreadProps(__spreadValues({}, opts), {
   ac: opts.ac || new AbortController(),
   ee: new import_node_events.EventEmitter(),
   from,
-  cmd
+  pieces,
+  args,
+  cmd: ""
 });
 function within(callback) {
   return storage.run(__spreadValues({}, getStore()), callback);
 }
 var $ = new Proxy(
+  // prettier-ignore
   function(pieces, ...args) {
     const opts = getStore();
     if (!Array.isArray(pieces)) {
@@ -460,17 +457,8 @@ var $ = new Proxy(
       };
     }
     const from = Fail.getCallerLocation();
-    if (pieces.some((p) => p == null))
-      throw new Fail(`Malformed command at ${from}`);
-    checkShell();
-    checkQuote();
-    const cmd = (0, import_vendor_core2.buildCmd)(
-      $.quote,
-      pieces,
-      args
-    );
-    box.push(getSnapshot(opts, from, cmd));
-    const pp = new ProcessPromise(import_util.noop);
+    const cb = () => cb[SHOT] = getSnapshot(opts, from, pieces, args);
+    const pp = new ProcessPromise(cb);
     if (!pp.isHalted()) pp.run();
     return pp.sync ? pp.output : pp;
   },
@@ -493,7 +481,7 @@ var _ProcessPromise = class _ProcessPromise extends Promise {
     let reject;
     super((...args) => {
       ;
-      [resolve, reject] = args;
+      [resolve = import_util.noop, reject = import_util.noop] = args;
       executor(...args);
     });
     this._stage = "initial";
@@ -502,21 +490,36 @@ var _ProcessPromise = class _ProcessPromise extends Promise {
     this._stdin = new import_vendor_core2.VoidStream();
     this._zurk = null;
     this._output = null;
-    this._reject = import_util.noop;
-    this._resolve = import_util.noop;
     // Stream-like API
     this.writable = true;
-    const snapshot = box.loot();
+    const snapshot = executor[SHOT];
     if (snapshot) {
       this._snapshot = snapshot;
       this._resolve = resolve;
-      this._reject = (v) => {
-        reject(v);
-        if (this.sync) throw v;
-      };
+      this._reject = reject;
       if (snapshot.halt) this._stage = "halted";
+      try {
+        this.build();
+      } catch (err) {
+        this.finalize(ProcessOutput.fromError(err), true);
+      }
     } else _ProcessPromise.disarm(this);
   }
+  // prettier-ignore
+  build() {
+    const $2 = this._snapshot;
+    if (!$2.shell)
+      throw new Fail(`No shell is available: ${Fail.DOCS_URL}/shell`);
+    if (!$2.quote)
+      throw new Fail(`No quote function is defined: ${Fail.DOCS_URL}/quotes`);
+    if ($2.pieces.some((p) => p == null))
+      throw new Fail(`Malformed command at ${$2.from}`);
+    $2.cmd = (0, import_vendor_core2.buildCmd)(
+      $2.quote,
+      $2.pieces,
+      $2.args
+    );
+  }
   run() {
     var _a, _b, _c, _d;
     if (this.isRunning() || this.isSettled()) return this;
@@ -555,7 +558,7 @@ var _ProcessPromise = class _ProcessPromise extends Promise {
             ctx.cmd = self.fullCmd;
             cb();
           },
-          (error) => ctx.on.end({ error, status: null, signal: null, duration: 0, ctx }, ctx)
+          (error) => self.finalize(ProcessOutput.fromError(error))
         )) || cb();
       },
       on: {
@@ -573,7 +576,7 @@ var _ProcessPromise = class _ProcessPromise extends Promise {
         end: (data, c) => {
           const { error, status, signal, duration, ctx: { store } } = data;
           const { stdout, stderr } = store;
-          const output = self._output = new ProcessOutput({
+          const output = new ProcessOutput({
             code: status,
             signal,
             error,
@@ -584,18 +587,27 @@ var _ProcessPromise = class _ProcessPromise extends Promise {
           $2.log({ kind: "end", signal, exitCode: status, duration, error, verbose: self.isVerbose(), id });
           if (stdout.length && (0, import_util.getLast)((0, import_util.getLast)(stdout)) !== BR_CC) c.on.stdout(EOL, c);
           if (stderr.length && (0, import_util.getLast)((0, import_util.getLast)(stderr)) !== BR_CC) c.on.stderr(EOL, c);
-          if (output.ok || self.isNothrow()) {
-            self._stage = "fulfilled";
-            self._resolve(output);
-          } else {
-            self._stage = "rejected";
-            self._reject(output);
-          }
+          self.finalize(output);
         }
       }
     });
     return this;
   }
+  finalize(output, legacy = false) {
+    this._output = output;
+    if (output.ok || this.isNothrow()) {
+      this._stage = "fulfilled";
+      this._resolve(output);
+    } else {
+      this._stage = "rejected";
+      if (legacy) {
+        this._resolve(output);
+        throw output.cause || output;
+      }
+      this._reject(output);
+      if (this.sync) throw output;
+    }
+  }
   _pipe(source, dest, ...args) {
     if ((0, import_util.isStringLiteral)(dest, ...args))
       return this.pipe[source](
@@ -865,12 +877,14 @@ Object.defineProperty(_ProcessPromise.prototype, "pipe", { get() {
 var ProcessPromise = _ProcessPromise;
 var _ProcessOutput = class _ProcessOutput extends Error {
   // prettier-ignore
-  constructor(code, signal = null, stdout = "", stderr = "", stdall = "", message = "", duration = 0, error = null, from = "", store = { stdout: [stdout], stderr: [stderr], stdall: [stdall] }) {
+  constructor(code = null, signal = null, stdout = "", stderr = "", stdall = "", message = "", duration = 0, error = null, from = "", store = { stdout: [stdout], stderr: [stderr], stdall: [stdall] }) {
     super(message);
     const dto = code !== null && typeof code === "object" ? code : { code, signal, duration, error, from, store };
     Object.defineProperties(this, {
       _dto: { value: dto, enumerable: false },
-      cause: { value: dto.error, enumerable: false },
+      cause: { get() {
+        return dto.error;
+      }, enumerable: false },
       stdout: { get: (0, import_util.once)(() => (0, import_util.bufArrJoin)(dto.store.stdout)) },
       stderr: { get: (0, import_util.once)(() => (0, import_util.bufArrJoin)(dto.store.stderr)) },
       stdall: { get: (0, import_util.once)(() => (0, import_util.bufArrJoin)(dto.store.stdall)) },
@@ -919,8 +933,7 @@ var _ProcessOutput = class _ProcessOutput extends Error {
     return encoding === "utf8" ? this.toString() : this.buffer().toString(encoding);
   }
   lines(delimiter) {
-    box.push(delimiter);
-    return [...this];
+    return (0, import_util.iteratorToArray)(this[Symbol.iterator](delimiter));
   }
   toString() {
     return this.stdall;
@@ -931,9 +944,9 @@ var _ProcessOutput = class _ProcessOutput extends Error {
   [Symbol.toPrimitive]() {
     return this.valueOf();
   }
-  *[Symbol.iterator]() {
+  // prettier-ignore
+  *[Symbol.iterator](dlmtr = this._dto.delimiter || $.delimiter || DLMTR) {
     const memo = [];
-    const dlmtr = box.loot() || this._dto.delimiter || $.delimiter || DLMTR;
     for (const chunk of this._dto.store.stdall) {
       yield* __yieldStar((0, import_util.getLines)(chunk, memo, dlmtr));
     }
@@ -949,6 +962,11 @@ var _ProcessOutput = class _ProcessOutput extends Error {
   duration: ${this.duration}
 }`;
   }
+  static fromError(error) {
+    const output = new _ProcessOutput();
+    output._dto.error = error;
+    return output;
+  }
 };
 _ProcessOutput.getExitMessage = Fail.formatExitMessage;
 _ProcessOutput.getErrorMessage = Fail.formatErrorMessage;
@@ -981,13 +999,6 @@ try {
   if ((0, import_util.isString)(postfix)) $.postfix = postfix;
 } catch (err) {
 }
-function checkShell() {
-  if (!$.shell) throw new Fail(`No shell is available: ${Fail.DOCS_URL}/shell`);
-}
-function checkQuote() {
-  if (!$.quote)
-    throw new Fail(`No quote function is defined: ${Fail.DOCS_URL}/quotes`);
-}
 var cwdSyncHook;
 function syncProcessCwd(flag = true) {
   cwdSyncHook = cwdSyncHook || (0, import_node_async_hooks.createHook)({
build/core.d.ts
@@ -4,6 +4,7 @@
 import { Buffer } from 'node:buffer';
 import cp, { type ChildProcess, type IOType, type StdioOptions } from 'node:child_process';
 import { type Encoding } from 'node:crypto';
+import { EventEmitter } from 'node:events';
 import { type Readable, type Writable } from 'node:stream';
 import { inspect } from 'node:util';
 import { Fail } from './error.js';
@@ -18,6 +19,7 @@ export { chalk, which, ps } from './vendor-core.js';
 export { type Duration, quote, quotePowerShell } from './util.js';
 declare const CWD: unique symbol;
 declare const SYNC: unique symbol;
+declare const SHOT: unique symbol;
 export interface Options {
     [CWD]: string;
     [SYNC]: boolean;
@@ -49,6 +51,14 @@ export interface Options {
     delimiter?: string | RegExp;
 }
 export declare const defaults: Options;
+type Snapshot = Options & {
+    from: string;
+    pieces: TemplateStringsArray;
+    args: string[];
+    cmd: string;
+    ee: EventEmitter;
+    ac: AbortController;
+};
 export interface Shell<S = false, R = S extends true ? ProcessOutput : ProcessPromise> {
     (pieces: TemplateStringsArray, ...args: any[]): R;
     <O extends Partial<Options> = Partial<Options>, R = O extends {
@@ -64,6 +74,11 @@ export type $ = Shell & Options;
 export declare const $: $;
 type ProcessStage = 'initial' | 'halted' | 'running' | 'fulfilled' | 'rejected';
 type Resolve = (out: ProcessOutput) => void;
+type Reject = (error: ProcessOutput | Error) => void;
+type PromiseCallback = {
+    (resolve: Resolve, reject: Reject): void;
+    [SHOT]?: Snapshot;
+};
 type PromisifiedStream<D extends Writable> = D & PromiseLike<ProcessOutput & D>;
 type PipeMethod = {
     (dest: TemplateStringsArray, ...args: any[]): ProcessPromise;
@@ -81,10 +96,12 @@ export declare class ProcessPromise extends Promise<ProcessOutput> {
     private _stdin;
     private _zurk;
     private _output;
-    private _reject;
     private _resolve;
-    constructor(executor: (resolve: Resolve, reject: Resolve) => void);
+    private _reject;
+    constructor(executor: PromiseCallback);
+    private build;
     run(): ProcessPromise;
+    private finalize;
     pipe: PipeMethod & {
         [key in keyof TSpawnStore]: PipeMethod;
     };
@@ -156,7 +173,7 @@ export declare class ProcessOutput extends Error {
     stderr: string;
     stdall: string;
     constructor(dto: ProcessDto);
-    constructor(code: number | null, signal: NodeJS.Signals | null, stdout: string, stderr: string, stdall: string, message: string, duration?: number);
+    constructor(code?: number | null, signal?: NodeJS.Signals | null, stdout?: string, stderr?: string, stdall?: string, message?: string, duration?: number);
     get exitCode(): number | null;
     get signal(): NodeJS.Signals | null;
     get duration(): number;
@@ -170,12 +187,13 @@ export declare class ProcessOutput extends Error {
     toString(): string;
     valueOf(): string;
     [Symbol.toPrimitive](): string;
-    [Symbol.iterator](): Iterator<string>;
+    [Symbol.iterator](dlmtr?: Options['delimiter']): Iterator<string>;
     [inspect.custom](): string;
     static getExitMessage: typeof Fail.formatExitMessage;
     static getErrorMessage: typeof Fail.formatErrorMessage;
     static getErrorDetails: typeof Fail.formatErrorDetails;
     static getExitCodeInfo: typeof Fail.getExitCodeInfo;
+    static fromError(error: Error): ProcessOutput;
 }
 export declare function usePowerShell(): void;
 export declare function usePwsh(): void;
build/util.cjs
@@ -18,6 +18,7 @@ __export(util_exports, {
   identity: () => identity,
   isString: () => isString,
   isStringLiteral: () => import_vendor_core.isStringLiteral,
+  iteratorToArray: () => iteratorToArray,
   noop: () => noop,
   once: () => once,
   parseBool: () => parseBool,
@@ -97,6 +98,14 @@ var getLines = (chunk, next, delimiter) => {
   next.push(lines.pop());
   return lines;
 };
+var iteratorToArray = (it) => {
+  const arr = [];
+  let entry;
+  while (!(entry = it.next()).done) {
+    arr.push(entry.value);
+  }
+  return arr;
+};
 /* c8 ignore next 100 */
 // Annotate the CommonJS export names for ESM import in node:
 0 && (module.exports = {
@@ -107,6 +116,7 @@ var getLines = (chunk, next, delimiter) => {
   identity,
   isString,
   isStringLiteral,
+  iteratorToArray,
   noop,
   once,
   parseBool,
build/util.d.ts
@@ -24,3 +24,4 @@ export declare const proxyOverride: <T extends object>(origin: T, ...fallbacks:
 export declare const toCamelCase: (str: string) => string;
 export declare const parseBool: (v: string) => boolean | string;
 export declare const getLines: (chunk: Buffer | string, next: (string | undefined)[], delimiter: string | RegExp) => string[];
+export declare const iteratorToArray: <T>(it: Iterator<T>) => T[];
build/vendor-core.d.ts
@@ -272,7 +272,7 @@ export type TSpawnStore = {
 	stderr: TSpawnStoreChunks;
 	stdall: TSpawnStoreChunks;
 };
-export type TSpawnResult = {
+type TSpawnResult = {
 	stderr: string;
 	stdout: string;
 	stdall: string;
src/core.ts
@@ -37,12 +37,12 @@ import {
   ps,
   VoidStream,
   type TSpawnStore,
-  type TSpawnResult,
 } from './vendor-core.ts'
 import {
   type Duration,
   isString,
   isStringLiteral,
+  iteratorToArray,
   getLast,
   getLines,
   noop,
@@ -68,6 +68,7 @@ export { type Duration, quote, quotePowerShell } from './util.ts'
 const CWD = Symbol('processCwd')
 const SYNC = Symbol('syncExec')
 const EPF = Symbol('end-piped-from')
+const SHOT = Symbol('snapshot')
 const EOL = Buffer.from(_EOL)
 const BR_CC = '\n'.charCodeAt(0)
 const DLMTR = /\r?\n/
@@ -142,6 +143,8 @@ export const defaults: Options = resolveDefaults({
 
 type Snapshot = Options & {
   from: string
+  pieces: TemplateStringsArray
+  args: string[]
   cmd: string
   ee: EventEmitter
   ac: AbortController
@@ -160,24 +163,23 @@ export interface Shell<
   }
 }
 
-// Internal storages
 const storage = new AsyncLocalStorage<Options>()
-const box = (<B extends Snapshot | Snapshot['delimiter']>(box: B[] = []) => ({
-  push(item: B): void {
-    if (box.length > 0) throw new Fail(`Box is busy`)
-    box.push(item)
-  },
-  loot: box.pop.bind(box) as <T extends B>() => T | undefined,
-}))()
 
 const getStore = () => storage.getStore() || defaults
 
-const getSnapshot = (opts: Options, from: string, cmd: string): Snapshot => ({
+const getSnapshot = (
+  opts: Options,
+  from: string,
+  pieces: TemplateStringsArray,
+  args: any[]
+): Snapshot => ({
   ...opts,
   ac: opts.ac || new AbortController(),
   ee: new EventEmitter(),
   from,
-  cmd,
+  pieces,
+  args,
+  cmd: '',
 })
 
 export function within<R>(callback: () => R): R {
@@ -188,6 +190,7 @@ export function within<R>(callback: () => R): R {
 export type $ = Shell & Options
 
 export const $: $ = new Proxy<$>(
+  // prettier-ignore
   function (pieces: TemplateStringsArray | Partial<Options>, ...args: any[]) {
     const opts = getStore()
     if (!Array.isArray(pieces)) {
@@ -196,19 +199,8 @@ export const $: $ = new Proxy<$>(
       }
     }
     const from = Fail.getCallerLocation()
-    if (pieces.some((p) => p == null))
-      throw new Fail(`Malformed command at ${from}`)
-
-    checkShell()
-    checkQuote()
-
-    const cmd = buildCmd(
-      $.quote as typeof quote,
-      pieces as TemplateStringsArray,
-      args
-    ) as string
-    box.push(getSnapshot(opts, from, cmd))
-    const pp = new ProcessPromise(noop)
+    const cb: PromiseCallback = () => (cb[SHOT] = getSnapshot(opts, from, pieces as TemplateStringsArray, args))
+    const pp = new ProcessPromise(cb)
 
     if (!pp.isHalted()) pp.run()
 
@@ -234,6 +226,13 @@ type ProcessStage = 'initial' | 'halted' | 'running' | 'fulfilled' | 'rejected'
 
 type Resolve = (out: ProcessOutput) => void
 
+type Reject = (error: ProcessOutput | Error) => void
+
+type PromiseCallback = {
+  (resolve: Resolve, reject: Reject): void
+  [SHOT]?: Snapshot
+}
+
 type PromisifiedStream<D extends Writable> = D & PromiseLike<ProcessOutput & D>
 
 type PipeDest = Writable | ProcessPromise | TemplateStringsArray | string
@@ -254,29 +253,46 @@ export class ProcessPromise extends Promise<ProcessOutput> {
   private _stdin = new VoidStream()
   private _zurk: ReturnType<typeof exec> | null = null
   private _output: ProcessOutput | null = null
-  private _reject: Resolve = noop
-  private _resolve: Resolve = noop
+  private _resolve!: Resolve
+  private _reject!: Reject
 
-  constructor(executor: (resolve: Resolve, reject: Resolve) => void) {
+  constructor(executor: PromiseCallback) {
     let resolve: Resolve
-    let reject: Resolve
+    let reject: Reject
     super((...args) => {
-      ;[resolve, reject] = args
+      ;[resolve = noop, reject = noop] = args
       executor(...args)
     })
 
-    const snapshot = box.loot<Snapshot>()
+    const snapshot = executor[SHOT]
     if (snapshot) {
       this._snapshot = snapshot
       this._resolve = resolve!
-      this._reject = (v: ProcessOutput) => {
-        reject!(v)
-        if (this.sync) throw v
-      }
+      this._reject = reject!
       if (snapshot.halt) this._stage = 'halted'
+      try {
+        this.build()
+      } catch (err) {
+        this.finalize(ProcessOutput.fromError(err as Error), true)
+      }
     } else ProcessPromise.disarm(this)
   }
-
+  // prettier-ignore
+  private build(): void {
+    const $ = this._snapshot
+    if (!$.shell)
+      throw new Fail(`No shell is available: ${Fail.DOCS_URL}/shell`)
+    if (!$.quote)
+      throw new Fail(`No quote function is defined: ${Fail.DOCS_URL}/quotes`)
+    if ($.pieces.some((p) => p == null))
+      throw new Fail(`Malformed command at ${$.from}`)
+
+    $.cmd = buildCmd(
+      $.quote!,
+      $.pieces as TemplateStringsArray,
+      $.args
+    ) as string
+  }
   run(): ProcessPromise {
     if (this.isRunning() || this.isSettled()) return this // The _run() can be called from a few places.
     this._stage = 'running'
@@ -317,7 +333,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
             ctx.cmd = self.fullCmd
             cb()
           },
-          error => ctx.on.end!({ error, status: null, signal: null, duration: 0, ctx } as TSpawnResult, ctx)
+          error => self.finalize(ProcessOutput.fromError(error))
         ) || cb()
       },
       on: {
@@ -337,7 +353,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
         end: (data, c) => {
           const { error, status, signal, duration, ctx: {store} } = data
           const { stdout, stderr } = store
-          const output = self._output = new ProcessOutput({
+          const output = new ProcessOutput({
             code: status,
             signal,
             error,
@@ -352,13 +368,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
           if (stdout.length && getLast(getLast(stdout)) !== BR_CC) c.on.stdout!(EOL, c)
           if (stderr.length && getLast(getLast(stderr)) !== BR_CC) c.on.stderr!(EOL, c)
 
-          if (output.ok || self.isNothrow()) {
-            self._stage = 'fulfilled'
-            self._resolve(output)
-          } else {
-            self._stage = 'rejected'
-            self._reject(output)
-          }
+          self.finalize(output)
         },
       },
     })
@@ -366,6 +376,22 @@ export class ProcessPromise extends Promise<ProcessOutput> {
     return this
   }
 
+  private finalize(output: ProcessOutput, legacy = false): void {
+    this._output = output
+    if (output.ok || this.isNothrow()) {
+      this._stage = 'fulfilled'
+      this._resolve(output)
+    } else {
+      this._stage = 'rejected'
+      if (legacy) {
+        this._resolve(output) // to avoid unhandledRejection alerts
+        throw output.cause || output
+      }
+      this._reject(output)
+      if (this.sync) throw output
+    }
+  }
+
   // Essentials
   pipe!: PipeMethod & {
     [key in keyof TSpawnStore]: PipeMethod
@@ -716,17 +742,17 @@ export class ProcessOutput extends Error {
   stdall!: string
   constructor(dto: ProcessDto)
   constructor(
-    code: number | null,
-    signal: NodeJS.Signals | null,
-    stdout: string,
-    stderr: string,
-    stdall: string,
-    message: string,
+    code?: number | null,
+    signal?: NodeJS.Signals | null,
+    stdout?: string,
+    stderr?: string,
+    stdall?: string,
+    message?: string,
     duration?: number
   )
   // prettier-ignore
   constructor(
-    code: number | null | ProcessDto,
+    code: number | null | ProcessDto = null,
     signal: NodeJS.Signals | null = null,
     stdout: string = '',
     stderr: string = '',
@@ -744,7 +770,7 @@ export class ProcessOutput extends Error {
 
     Object.defineProperties(this, {
       _dto: { value: dto, enumerable: false },
-      cause: { value: dto.error, enumerable: false },
+      cause: { get() { return dto.error }, enumerable: false },
       stdout: { get: once(() => bufArrJoin(dto.store.stdout)) },
       stderr: { get: once(() => bufArrJoin(dto.store.stderr)) },
       stdall: { get: once(() => bufArrJoin(dto.store.stdall)) },
@@ -806,8 +832,7 @@ export class ProcessOutput extends Error {
   }
 
   lines(delimiter?: string | RegExp): string[] {
-    box.push(delimiter)
-    return [...this]
+    return iteratorToArray(this[Symbol.iterator](delimiter))
   }
 
   override toString(): string {
@@ -821,12 +846,9 @@ export class ProcessOutput extends Error {
   [Symbol.toPrimitive](): string {
     return this.valueOf()
   }
-
-  *[Symbol.iterator](): Iterator<string> {
+  // prettier-ignore
+  *[Symbol.iterator](dlmtr: Options['delimiter'] = this._dto.delimiter || $.delimiter || DLMTR): Iterator<string> {
     const memo: (string | undefined)[] = []
-    // prettier-ignore
-    const dlmtr = box.loot<Options['delimiter']>() || this._dto.delimiter || $.delimiter || DLMTR
-
     for (const chunk of this._dto.store.stdall) {
       yield* getLines(chunk, memo, dlmtr)
     }
@@ -855,6 +877,12 @@ export class ProcessOutput extends Error {
   static getErrorDetails = Fail.formatErrorDetails
 
   static getExitCodeInfo = Fail.getExitCodeInfo
+
+  static fromError(error: Error): ProcessOutput {
+    const output = new ProcessOutput()
+    output._dto.error = error
+    return output
+  }
 }
 
 export function usePowerShell() {
@@ -886,15 +914,6 @@ try {
   if (isString(postfix)) $.postfix = postfix
 } catch (err) {}
 
-function checkShell() {
-  if (!$.shell) throw new Fail(`No shell is available: ${Fail.DOCS_URL}/shell`)
-}
-
-function checkQuote() {
-  if (!$.quote)
-    throw new Fail(`No quote function is defined: ${Fail.DOCS_URL}/quotes`)
-}
-
 let cwdSyncHook: AsyncHook
 
 export function syncProcessCwd(flag: boolean = true) {
src/util.ts
@@ -154,3 +154,12 @@ export const getLines = (
   next.push(lines.pop())
   return lines
 }
+
+export const iteratorToArray = <T>(it: Iterator<T>): T[] => {
+  const arr = []
+  let entry
+  while (!(entry = it.next()).done) {
+    arr.push(entry.value)
+  }
+  return arr
+}
src/vendor-core.ts
@@ -20,7 +20,6 @@ import { bus } from './internals.ts'
 export {
   type TSpawnStore,
   type TSpawnStoreChunks,
-  type TSpawnResult,
   exec,
   buildCmd,
   isStringLiteral,
test/core.test.js
@@ -224,6 +224,10 @@ describe('core', () => {
         assert.ok(e instanceof Fail)
         assert.match(e.message, /malformed/i)
       }
+
+      const o = await $({ nothrow: true })`\033`
+      assert.equal(o.ok, false)
+      assert.match(o.cause.message, /malformed/i)
     })
 
     test('snapshots works', async () => {
test-d/core.test-d.ts
@@ -57,7 +57,7 @@ expectType<ProcessOutput>(new ProcessOutput({
 
 expectType<ProcessOutput>(new ProcessOutput(null, null, '', '', '', '', 1))
 expectType<ProcessOutput>(new ProcessOutput(null, null, '', '', '', ''))
-expectError(new ProcessOutput(null, null))
+expectError(new ProcessOutput('1'))
 
 expectType<'banana'>(within(() => 'apple' as 'banana'))
 
.size-limit.json
@@ -15,7 +15,7 @@
       "README.md",
       "LICENSE"
     ],
-    "limit": "121.45 kB",
+    "limit": "122.50 kB",
     "brotli": false,
     "gzip": false
   },
@@ -29,14 +29,14 @@
       "build/globals.js",
       "build/deno.js"
     ],
-    "limit": "812.50 kB",
+    "limit": "813.00 kB",
     "brotli": false,
     "gzip": false
   },
   {
     "name": "libdefs",
     "path": "build/*.d.ts",
-    "limit": "39.65 kB",
+    "limit": "40.20 kB",
     "brotli": false,
     "gzip": false
   },
@@ -62,7 +62,7 @@
       "README.md",
       "LICENSE"
     ],
-    "limit": "869.00 kB",
+    "limit": "870.05 kB",
     "brotli": false,
     "gzip": false
   }
package-lock.json
@@ -17,7 +17,7 @@
         "@size-limit/file": "11.2.0",
         "@types/fs-extra": "11.0.4",
         "@types/minimist": "1.2.5",
-        "@types/node": "24.0.15",
+        "@types/node": "24.1.0",
         "@types/which": "3.0.4",
         "@webpod/ingrid": "1.1.1",
         "@webpod/ps": "0.1.4",
@@ -2126,9 +2126,9 @@
       "license": "MIT"
     },
     "node_modules/@types/node": {
-      "version": "24.0.15",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.15.tgz",
-      "integrity": "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA==",
+      "version": "24.1.0",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
+      "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
package.json
@@ -109,7 +109,7 @@
     "@size-limit/file": "11.2.0",
     "@types/fs-extra": "11.0.4",
     "@types/minimist": "1.2.5",
-    "@types/node": "24.0.15",
+    "@types/node": "24.1.0",
     "@types/which": "3.0.4",
     "@webpod/ingrid": "1.1.1",
     "@webpod/ps": "0.1.4",