32

I have the following script,

$createZip = {
    Param ([String]$source, [String]$zipfile)
    Process { 
        echo "zip: $source`n     --> $zipfile"
        throw "test"
    }
}

try {
    Start-Job -ScriptBlock $createZip -ArgumentList "abd", "acd"  
    echo "**Don't reach here if error**"
    LogThezippedFile
}
catch {
    echo "Captured: "
    $_ | fl * -force
}
Get-Job | Wait-Job 
Get-Job | receive-job 
Get-Job | Remove-Job 

However, the exception raised in another powershell instance cannot be captured. What's the best way to capture the exception?

Id              Name            State      HasMoreData     Location             Command                  
--              ----            -----      -----------     --------             -------                  
343             Job343          Running    True            localhost            ...                      
**Don't reach here if error**
343             Job343          Failed     True            localhost            ...                      
zip: abd
     --> acd
Receive-Job : test
At line:18 char:22
+ Get-Job | receive-job <<<<  
    + CategoryInfo          : OperationStopped: (test:String) [Receive-Job], RuntimeException
    + FullyQualifiedErrorId : test
ca9163d9
  • 27,283
  • 64
  • 210
  • 413
  • I updated my answer to show you how in your previous question. – Andy Arismendi Jan 05 '12 at 23:05
  • It's not clear what you are trying to accomplish with the job. It seems like you want both asynchronous and synchronous behaviour at the same time, which is impossible. When do you want to execute the next line in your script? – zdan Jan 06 '12 at 01:03

4 Answers4

44

Using throw will change the job object's State property to "Failed". The key is to use the job object returned from Start-Job or Get-Job and check the State property. You can then access the exception message from the job object itself.

Per your request I updated the example to also include concurrency.

$createZip = {
    Param ( [String] $source, [String] $zipfile )

    if ($source -eq "b") {
        throw "Failed to create $zipfile"
    } else {
        return "Successfully created $zipfile"
    }
}

$jobs = @()
$sources = "a", "b", "c"

foreach ($source in $sources) {
    $jobs += Start-Job -ScriptBlock $createZip -ArgumentList $source, "${source}.zip"
}

Wait-Job -Job $jobs | Out-Null

foreach ($job in $jobs) {
    if ($job.State -eq 'Failed') {
        Write-Host ($job.ChildJobs[0].JobStateInfo.Reason.Message) -ForegroundColor Red
    } else {
        Write-Host (Receive-Job $job) -ForegroundColor Green 
    }
}
Andy Arismendi
  • 50,577
  • 16
  • 107
  • 124
  • Thanks, I updated the question - I don't want the logging after the zipping be executed if error occurs. Is it possible? – ca9163d9 Jan 05 '12 at 23:17
  • Thanks. However the `Wait-Job` will block the execution of the statements in the try block, which will be run in a big loop. My purpose of using start-job is to parallel the task in the main loop and zipping... It sounds like I am looking some asynchronized executing... – ca9163d9 Jan 05 '12 at 23:28
  • @NickW Sorry posted without checking. There was a problem with it. I just updated updated a tested version, give this a try. – Andy Arismendi Jan 05 '12 at 23:29
  • @NickW yes the example I provided only addresses receive the error from the background job. If you want asynchronous looping support that'll cost extra ;-) (It's very doable though). – Andy Arismendi Jan 05 '12 at 23:31
  • Thanks. I think I will need to change the `$Jobs += start-job ` to include some extra information. Maybe $Jobs should be a dictionary. – ca9163d9 Jan 06 '12 at 18:18
14

This should be a comment really, but I don't have the reputation to leave comments.

My answer is that you should use Andy Arismendi's answer, but also output $job.ChildJobs[0].Error

As $job.ChildJobs[0].JobStateInfo.Reason.Message isn't always useful.

waxingsatirical
  • 584
  • 1
  • 7
  • 11
  • 1
    For me, access to the exception object was `$job.ChildJobs[0].JobStateInfo.Reason`. I was also able to 'rethrow' the error. ChicldJobs[0].Error was empty. – Patrick Mar 01 '16 at 02:27
  • 2
    `$job.ChildJobs[0].Error` returns all non-terminating errors. `$job.ChildJobs[0].JobStateInfo.State` returns terminating error if any (with State == Failed). [source](https://windowsserver.uservoice.com/forums/301869-powershell/suggestions/13399002-powershell-jobs-error-handling) – DarkLite1 Jul 09 '18 at 08:42
8

I was able to "rethrow" the exception in the main thread by using:

Receive-Job $job -ErrorAction Stop

I'll my use case as an example. It can easily be applied to the OP.

$code = {
    $Searcher = New-Object -ComObject Microsoft.Update.Searcher
    #Errors from Search are not terminating, but will be present in the output none the less.
    $Results = $Searcher.Search('IsInstalled=0  and IsHidden=0')
    $Results.Updates
};
$job = Start-Job -ScriptBlock $code;
$consume = Wait-Job $job -Timeout 600;

if ($job.state -eq 'Running') {
    Stop-Job $job
    throw 'Windows update searcher took more than 10 minutes. Aborting' 
};

#Captures and throws any exception in the job output
Receive-Job $job -ErrorAction Stop;
Write-Host "Finished with no errors"; #this will not print if there was an error

Works in v2.0.

Note that if the error within the job is non-terminating, the subsequent lines will continue to execute. But, this will not be obvious in the output returned from Receive-Job, as Receive-Job "terminates half way thorugh" - it throws out of it's self when the error object is encountered.

One way to avoid that is to wrap the whole block in a try {} catch{throw;}

Also, Job state will not be 'Failed' if the exception is non-terminating

Patrick
  • 1,089
  • 14
  • 17
4

TLDR:

# Works with both terminating and non terminating errors
$j = start-job {1/0} | wait-job; try { receive-job $j -ErrorAction Stop } catch { "err $_" } 

Zach Johnson
  • 682
  • 7
  • 14
majkinetor
  • 8,730
  • 9
  • 54
  • 72